Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .pi/extensions/nmg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { defineTool } from "@earendil-works/pi-coding-agent";
import {
captureStateInvocation,
COORDINATION_OPERATIONS,
stateOperation,
renderStateOutput,
STATE_BOARD_READ_OPERATIONS,
Expand Down Expand Up @@ -707,109 +708,109 @@
"info",
);
};
const nmgMenuHandler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
const parts = args.trim().split(/\s+/).filter(Boolean);
const [sub, ...rest] = parts;
const restText = rest.join(" ");
switch (sub) {
case "recall": {
recallCommand(rest, ctx);
return;
}
case "wake": {
const current = readWakeConfig();
const [wcmd, wvalRaw] = restText.split(/\s+/);
const wval = Number(wvalRaw);
switch (wcmd) {
case "on":
writeWakeConfig({ ...current, enabled: true });
ctx.ui.notify("黑板唤醒已开启(世界频道+活跃频道有新 open 条目会唤醒)", "info");
return;
case "off":
writeWakeConfig({ ...current, enabled: false });
ctx.ui.notify("黑板唤醒已关闭", "info");
return;
case "status": {
const budgetText = current.budget === 0 ? "不限制" : `${current.budget}/天`;
const cooldownText =
current.cooldownMs === 0 ? "无" : `${Math.round(current.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`黑板唤醒:${current.enabled ? "开" : "关"} · 预算 ${budgetText} · 冷却 ${cooldownText} · 轮询 ${Math.round(current.intervalMs / 1_000)} 秒 · 世界广播 ${current.worldBroadcast ? "开" : "关"}`,
"info",
);
return;
}
case "budget": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake budget N(0=不限制)", "warning");
return;
}
const budget = Math.max(0, Math.min(100, Math.round(wval)));
writeWakeConfig({ ...current, budget });
ctx.ui.notify(`黑板唤醒预算已设为 ${budget === 0 ? "不限制" : `${budget}/天`}`, "info");
return;
}
case "cooldown": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake cooldown M(0=无冷却)", "warning");
return;
}
const cooldownMs = wval === 0 ? 0 : Math.max(30_000, Math.round(wval * 60_000));
writeWakeConfig({ ...current, cooldownMs });
ctx.ui.notify(
`黑板唤醒冷却已设为 ${cooldownMs === 0 ? "无" : `${Math.round(cooldownMs / 60_000)} 分钟`}`,
"info",
);
return;
}
case "interval": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake interval S(秒,最小 5)", "warning");
return;
}
const intervalMs = Math.max(5_000, Math.round(wval * 1_000));
writeWakeConfig({ ...current, intervalMs });
ctx.ui.notify(`黑板唤醒轮询已设为 ${Math.round(intervalMs / 1_000)} 秒`, "info");
return;
}
case "world": {
// 无参切换;0 关、1 开。
const world =
wvalRaw === "0" ? false : wvalRaw === "1" ? true : !current.worldBroadcast;
writeWakeConfig({ ...current, worldBroadcast: world });
ctx.ui.notify(
world
? "已开启世界频道协作广播:协作类新条目会同时发到世界频道拉其他 agent"
: "已关闭世界频道协作广播",
"info",
);
return;
}
default: {
const enabled = !current.enabled;
writeWakeConfig({ ...current, enabled });
ctx.ui.notify(enabled ? "黑板唤醒已开启" : "黑板唤醒已关闭", "info");
}
}
return;
}
default: {
// 无参数 → 交互式选择菜单(可上下键选,不用手敲);未知子命令 → 总览提示。
if (parts.length === 0) {
await nmgInteractiveMenu(ctx);
return;
}
const wake = readWakeConfig();
const budgetText = wake.budget === 0 ? "不限制" : `${wake.budget}/天`;
const cooldownText =
wake.cooldownMs === 0 ? "无" : `${Math.round(wake.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`NMG 菜单:/nmg recall(召回折叠 ${recallCollapsed ? "开" : "关"}) · /nmg wake on|off|status|budget N|cooldown M|interval S(唤醒 ${wake.enabled ? "开" : "关"},预算 ${budgetText},冷却 ${cooldownText},轮询 ${Math.round(wake.intervalMs / 1_000)} 秒)`,
"info",
);
}
}
};

Check notice on line 813 in .pi/extensions/nmg/index.ts

View check run for this annotation

codefactor.io / CodeFactor

.pi/extensions/nmg/index.ts#L711-L813

Complex Method
// 交互式选择菜单:/nmg(无参数)弹出可上下键选的列表,避免手敲。带参数的
// 形式(/nmg recall、/nmg wake on 等)仍保留,供脚本/快捷路径直接调用。
// 挡位选择(select 预设值,避免随意输入溢出/无效);自由数字只走命令形式
Expand Down Expand Up @@ -876,67 +877,67 @@
await workingStateView.setPreview(onOff === "显示", ctx);
ctx.ui.notify(onOff === "显示" ? "状态预览已显示" : "状态预览已隐藏(状态本身不变)", "info");
};
const nmgInteractiveMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
if (!ctx.hasUI || typeof ctx.ui.select !== "function") {
ctx.ui.notify(
"当前模式不支持交互菜单,可用:/nmg recall、/nmg state on|off、/nmg wake on|off|status|budget N|cooldown M|interval S",
"warning",
);
return;
}
const choice = await ctx.ui.select(
"NMG 控制台",
nmgMenuOptions(recallCollapsed, readWakeConfig()),
);
if (!choice) return; // 用户取消
if (choice.startsWith("召回")) {
await nmgMenuHandler("recall", ctx);
return;
}
if (choice.startsWith("唤醒:开启/关闭")) {
const onOff = await ctx.ui.select("黑板唤醒", ["开启", "关闭"]);
if (!onOff) return;
const current = readWakeConfig();
writeWakeConfig({ ...current, enabled: onOff === "开启" });
ctx.ui.notify(onOff === "开启" ? "黑板唤醒已开启" : "黑板唤醒已关闭", "info");
return;
}
if (choice.startsWith("唤醒:世界广播")) {
const onOff = await ctx.ui.select("世界频道协作广播", ["开启", "关闭"]);
if (!onOff) return;
const current = readWakeConfig();
writeWakeConfig({ ...current, worldBroadcast: onOff === "开启" });
ctx.ui.notify(
onOff === "开启"
? "已开启世界频道协作广播:协作类新条目会同时发到世界频道拉其他 agent"
: "已关闭世界频道协作广播",
"info",
);
return;
}
if (choice.startsWith("唤醒:参数设置")) {
await nmgWakeParamMenu(ctx);
return;
}
if (choice.startsWith("当前状态:查看完整正文")) {
await workingStateView.readForUser(ctx);
return;
}
if (choice.startsWith("当前状态:显示/隐藏预览")) {
await nmgStatePreviewMenu(ctx);
return;
}
{
const wake = readWakeConfig();
const budgetText = wake.budget === 0 ? "不限制" : `${wake.budget}/天`;
const cooldownText =
wake.cooldownMs === 0 ? "无" : `${Math.round(wake.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`NMG:召回折叠 ${recallCollapsed ? "开" : "关"} · 唤醒 ${wake.enabled ? "开" : "关"}(预算 ${budgetText},冷却 ${cooldownText},轮询 ${Math.round(wake.intervalMs / 1_000)} 秒)`,
"info",
);
}
};

Check notice on line 940 in .pi/extensions/nmg/index.ts

View check run for this annotation

codefactor.io / CodeFactor

.pi/extensions/nmg/index.ts#L880-L940

Complex Method
// `/nmg state [on|off]` is the read-only state view and its preview switch.
// It dispatches before the menu handler so that handler keeps its own shape.
const nmgStateCommand = async (rest: string[], ctx: ExtensionCommandContext): Promise<void> => {
Expand Down Expand Up @@ -2368,6 +2369,14 @@
action: Type.Union([Type.Literal("read"), Type.Literal("write")]),
space: Type.Union([Type.Literal("ag"), Type.Literal("board")]),
operation: Type.Optional(Type.String()),
notification: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description:
"Explicit notice: key, scopes [direct|channel|event], optional recipients (stable IDs), invite:true to subscribe invitees and sender to taskId. Reuse key on retries.",
}),
),
eventId: Type.Optional(Type.String()),
event: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
text: Type.Optional(Type.String()),
}),
async execute(id, args, signal, onUpdate, ctx) {
Expand Down Expand Up @@ -2403,6 +2412,18 @@
return toolResult(details, renderStateOutput(result));
}
if (!coordinationToolsEnabled) throw new Error("Board coordination is disabled");
if (COORDINATION_OPERATIONS.has(operation)) {
const sessionId = ctx.sessionManager.getSessionId();
const identity = loadOrCreateAgentIdentity(sessionId);
const result = (await invoke("state", {
...args,
operation,
agentId: identity.id,
sessionId,
sourceSessionId: sessionId,
})) as NmgStateResult;
return toolResult(result, renderStateOutput(result));
}
return boardTool.execute(
id,
{
Expand Down
27 changes: 27 additions & 0 deletions agent-context.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ routes:
blocking: [check, test:product, build]
advisory: []

- id: board-coordination
paths:
- src/core/coordination-events.ts
- src/core/board-notifications.ts
- src/core/store/coordination.ts
- src/core/store/coordination-schema.ts
- src/core/store/board-notifications.ts
owners:
- docs/design/design.md
- docs/design/board-event-records.md
tests:
- tests/core/coordination-events.test.ts
- tests/core/coordination-history.test.ts
- tests/core/coordination-membership.test.ts
- tests/core/board-notifications.test.ts
- tests/cli/board-notifications.test.ts
- tests/cli/coordination-events.test.ts
- tests/integration/coordination-events.test.ts
- tests/integration/coordination-opencode.test.ts
- tests/extensions/dsh-state.test.ts
verify:
blocking: [check, test:product, build]
advisory: []

- id: core-memory
paths: [src/core/**]
owners:
Expand Down Expand Up @@ -221,6 +245,8 @@ routes:
- src/integration/working-state-input.ts
- src/integration/lab-capabilities.ts
- src/integration/state.ts
- src/integration/coordination-state.ts
- src/integration/board-delivery.ts
- src/integration/memory-reference.ts
- src/integration/reasoning-workspaces.ts
- src/integration/recall-budget.ts
Expand All @@ -242,6 +268,7 @@ routes:
tests/integration/lab-capabilities.test.ts,
tests/integration/recall-budget.test.ts,
tests/integration/state-client.test.ts,
tests/integration/coordination-events.test.ts,
tests/integration/memory-reference.test.ts,
tests/integration/memory-reference-snapshot.test.ts,
tests/integration/working-state-client.test.ts,
Expand Down
13 changes: 13 additions & 0 deletions claude-plugins/nmg-memory/agents/memory-copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,19 @@ server.registerTool(
action: z.enum(["read", "write"]),
space: z.enum(["ag", "board"]),
operation: z.enum(STATE_OPERATIONS).optional(),
notification: z
.record(z.string(), z.unknown())
.optional()
.describe(
"Explicit notice: key, scopes [direct|channel|event], optional recipients (stable IDs), invite:true enrolls invitees and sender in taskId; reuse key for retries.",
),
eventId: z.string().optional(),
event: z
.record(z.string(), z.unknown())
.optional()
.describe(
"Event create/mutation payload; mutations require expectedRevision, kind and text.",
),
text: z.string().optional(),
agentName: z.string().optional(),
taskId: z.string().optional().describe(nmgPrompts.board_task_id_parameter_description),
Expand Down
65 changes: 52 additions & 13 deletions docs/decisions/implemented/2026-10-17-board-event-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,27 @@ its ordinary messaging/find-Agent role rather than compatibly enhance it.

This record accepts the design semantics in the
[coordination event and Agent-status contract](../../design/board-event-records.md).
The implemented decision is the documentation/architecture choice, not a claim
that runtime event operations have shipped. Runtime evidence and remaining work
belong in the [completion ledger](../../design/completion-audit.md).
Runtime evidence, deployment boundaries and remaining work belong in the
[completion ledger](../../design/completion-audit.md).

World communication remains available. Event records add a coarse process account
and channel associations, with event-scoped updates and opt-out subscriptions.
Their organization does not impose an execution scheduler or a maintenance workflow.
Association and notification are separate: an event reference supplies context,
while explicit discussion/decision requests and key process changes address the
current subscriber group. Ordinary replies do not automatically re-notify the
event group; protocol acknowledgements are silent. Channel discussions continue
through their channel subscriptions. Agents organize the collaboration;
there is no automatic wake-chain length, round or event-count cap, and no forced
manual-resume threshold. The topic contract owns the complete routing and loop-risk
boundaries. Named recipients, channel subscribers and event subscribers are
separate audiences. An explicit discussion invitation delivers its first notice
to the invitees without requiring a prior channel subscription and automatically
enrolls them in the specified channel, with opt-out. Follow-up discussion uses
that channel subscription, not an event-wide rebroadcast. Event subscription alone
does not subscribe every related channel. Invitation retries do not undo a later
opt-out; invitation does not accept work. Overlapping audiences do not duplicate
one notification.
Agent status uses default idle, an optional self-authored note and automatic task
associations rather than inferred model/tool activity.

Expand All @@ -51,6 +65,14 @@ boundaries; the design does not replace them with a second shared memory graph.
privacy exposure and ambiguity where explicit task association suffices.
- A fresh broadcast and confirmation chain for every change: duplicates state,
depends on sender follow-up and reintroduces notification storms.
- Wake on every linked or directed message: confuses addressing with attention
and allows new acknowledgement entries to keep calling peers back.
- Treat event membership as subscription to every linked channel: leaks discussion
traffic across scopes and overrides independent subscription choices.
- Require channel subscription before the invitation can arrive, or leave an
invitee unenrolled: loses the initial invitation or follow-up discussion.
- Hard wake-chain limits or mandatory human rearming: interrupt the autonomous
self-organization the user wants instead of separating notifications from replies.

## Consequences

Expand All @@ -59,13 +81,30 @@ provides optional organization and subscription. Process closure, outcome and
ongoing effects remain separate. Default idle describes the task registry, not
proof of availability; manual status notes remain independent.

Documentation checks validate structure and links, not runtime delivery,
compatibility, archive durability, Agent behavior or coordination benefits.
No new storage, API, adapter or deployment is introduced by this decision commit.

## Deferred

Runtime schemas/migrations, capability exposure, event mutation and subscription
paths, delivery/version handling, archive policies and automatic task associations
need implementation and behavior tests against the topic contract. No API spelling,
physical storage format or new default runtime behavior is frozen here.
The shared Store uses additive schema migration and revision-checked event
transactions. Versioned directed notices reuse delivery receipts instead of
turning every update into a task or a world broadcast. Shared host delivery
awaits successful enqueue before recording a receipt; asynchronous enqueue failure
leaves the notice retryable without consuming its budget. Task-owner associations,
historical participation and notification opt-outs are separate facts. Public
status is derived rather than maintained as another task state machine.

Discussion notifications use original message identities rather than a process
revision for every chat reply. Invitation enrollment and outbox publication commit
with the message. Minimal author/key/hash/source tombstones survive message TTL
so retry cannot rejoin a departed invitee; withdrawing a route retires its queued
alerts without withdrawing another still-valid route. Pending reminders coalesce
without deleting source discussions. Explicit notices use note messages, keeping
claimable work separate. Bounded, source-labelled wake previews do not impose a
conversation-round limit.

Participant mutation checks and recorded effect ownership are cooperation rules,
not facility authorization or truth verification. Event archives outlive ordinary
message TTL and require explicit, reference-safe deletion. Existing host enqueue
and receipt boundaries do not promise exactly-once behavior across arbitrary
crashes or prove model comprehension.

Documentation checks establish structure and links only; source tests and host
integration establish their own narrower contracts. Deployment, live idle-Agent
wakeup, natural collaboration outcomes and coordination benefits are separate
from source validation. No shared service restart follows from this decision.
35 changes: 28 additions & 7 deletions docs/decisions/implemented/2026-10-17-board-event-records.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@
## Decision

本记录接受[事件记录、事件空间与 Agent 状态契约](../../design/board-event-records.md)中的设计语义。
这里落地的是文档/架构决策,不宣称事件运行接口已经交付;运行时证据和剩余工作由
[完成台账](../../design/completion-audit.md)记录。
运行时证据、部署边界和剩余工作由 [完成台账](../../design/completion-audit.md) 记录。

世界频道继续通信。事件记录增加粗粒度过程、频道关联,以及可退出的事件更新订阅,
不施加新的执行调度器,也不限定为维护流程。
事件关联只提供上下文,事件通知才对当前订阅者批量点名;显式讨论/决策请求和关键过程变化
可以通知;普通回复不自动重新通知整个事件,协议 ACK 静默,频道讨论按频道订阅继续提醒。
协作由 Agent 自己组织,
不设置自动唤醒链长度、轮数或事件通知次数硬上限,也不加必须由人恢复的门槛。
完整通知规则与循环风险边界由专题契约维护。
点名对象、频道订阅者和事件订阅者是独立受众。明确邀请的首条通知直接面向被邀请人,
不要求其先订阅;受邀自动订阅指定频道,后续按该频道订阅提醒,可以退出。
单纯事件订阅仍不意味着订阅所有关联频道;邀请重试不撤销后来的退订,受邀不等于接单。
同一次通知的重叠受众只接收一次。
Agent 状态采用默认空闲、自留说明与自动任务关联,不通过模型或工具活动推断忙闲。

专题契约是语义的单一 owner。现有黑板、身份、订阅、送达、认领和独立 verdict 继续负责各自边界,
Expand All @@ -36,17 +44,30 @@ Agent 状态采用默认空闲、自留说明与自动任务关联,不通过
- 维护专用编排:把发现问题的场景误当成通用事件空间的范围。
- 自动总结忙闲或复制私有状态:增加模型成本、隐私暴露和歧义,显式任务关联已足以表达名册状态。
- 每次变化重新广播并要求全员确认:重复状态、依赖发送方补发,也会重新引入通知风暴。
- 每条关联或定向消息都唤醒:混淆寻址与通知意图,新的确认条目仍能相互叫醒。
- 把事件参与等同订阅所有关联频道:让讨论流量跨范围扩散,覆盖独立订阅选择。
- 邀请前必须先订阅频道,或邀请后仍不登记订阅:前者漏掉首次邀请,后者漏掉后续讨论。
- 自动唤醒链硬上限或强制人工续开:截断用户期望的自组织协作,没有解决通知与回复的混淆。

## Consequences

设计保持增量兼容:旧消息/频道继续可用,事件历史提供可选的组织与订阅。
过程结束、工作结果与持续影响相互独立。默认空闲只描述任务登记,不证明实际可接单;
手写状态说明独立保留。

文档检查只验证结构与链接,不证明运行时投递、兼容、归档持久性、Agent 行为或协作收益。
本决策提交没有引入新存储、API、适配器或部署。
共享 Store 采用增量迁移及预期修订检查,事件快照、历史与通知事务性提交。
带版本的定向通知复用既有收据,不把更新变成任务或世界广播。
共享宿主送达先等待入队成功,再写收据;异步入队失败不消耗预算,通知仍可重试。
任务 owner 关联、历史参与和退订是独立事实;名册状态派生于任务,不维护另一套任务状态机。

## Deferred
讨论通知沿用原消息身份,不为每句聊天增加过程修订。邀请订阅登记及 outbox 与原消息事务性提交。
最小作者/请求键/摘要/源 ID 墓碑独立于消息 TTL,防止重试把退出者拉回;退出某范围撤掉其旧排队提醒,
不撤掉同一通知仍有效的其他范围。待提醒内容合并而不删除源讨论。显式通知采用 note,任务认领另走原协议。
带来源标签的唤醒预览控制单次输出大小,不限制会话讨论轮数。

运行时 schema/迁移、能力发现、事件修改与订阅路径、版本/投递处理、归档策略和自动任务关联,
仍需按专题契约实现并提供行为测试。本记录不冻结 API 拼写、物理存储格式或新的运行默认行为。
参与者修改检查及影响条目的记录 owner 是协作规则,不是设施授权或真实性认证。
事件档案独立于普通消息 TTL,只能在结束、影响解除且无必要引用时显式删除。
既有宿主入队与收据边界不承诺任意崩溃下的恰好一次,也不证明模型已经理解。

文档检查只建立结构与链接证据;源码测试和宿主集成分别证明其较窄契约。
部署、真实空闲 Agent 唤醒、自然协作结果和收益与源码验证分开,不因本决策重启共享服务。
Loading
Loading