pi-legion 5.0.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +858 -0
- package/CONTRIBUTING.md +33 -2
- package/README.md +10 -5
- package/agents/centurion.md +32 -7
- package/agents/scout.md +11 -0
- package/docs/contracts/abort-soft-interrupt-v1.md +162 -0
- package/docs/contracts/centurion-declared-phases-v1.md +104 -0
- package/docs/contracts/declarative-phase-machine-v5.md +43 -0
- package/docs/contracts/extension-inheritance-wiring-v1.md +88 -0
- package/docs/contracts/finished-cache-eviction-v1.md +169 -0
- package/docs/contracts/galaxy-scratch-v1.md +78 -0
- package/docs/contracts/legion-chart-frame-buckets-v1.md +201 -0
- package/docs/contracts/legion-main-screen-mount-v1.md +161 -0
- package/docs/contracts/legion-page-floor-v1.md +129 -0
- package/docs/contracts/legion-session-continuity-v1.md +118 -0
- package/docs/contracts/mailbox-delivery-v1.md +162 -0
- package/docs/contracts/notice-claim-v1.md +206 -0
- package/docs/contracts/partial-failure-exit-v1.md +235 -0
- package/docs/contracts/proc-sweep-v1.md +107 -0
- package/docs/contracts/provider-empty-generation-retry-v1.md +161 -0
- package/docs/contracts/stall-visibility-v1.md +218 -0
- package/docs/contracts/subagent-renewal-v1.md +5 -3
- package/docs/contracts/vexillatio-nonblocking-launch-v1.md +1 -0
- package/docs/contracts/viz-frame-alloc-v1.md +123 -0
- package/docs/contracts/wait-mail-preempt-v1.md +117 -0
- package/docs/contracts/wait-not-stop.md +1 -1
- package/docs/design.md +4 -1
- package/docs/inprocess-supervisor.md +9 -3
- package/docs/legion-v2-contract.md +122 -20
- package/docs/vexillatio-contract.md +5 -1
- package/index.ts +1903 -130
- package/package.json +5 -5
- package/skills/pi-legion/SKILL.md +2 -1
- package/src/agents/agents.ts +26 -1
- package/src/agents/settings.ts +67 -2
- package/src/api.ts +17 -0
- package/src/extension/child-guard.ts +2 -1
- package/src/extension/join.ts +54 -0
- package/src/extension/legion-commands.ts +15 -4
- package/src/extension/legion-doctor.ts +48 -6
- package/src/extension/legion-status.ts +44 -2
- package/src/extension/legion-tools.ts +1044 -109
- package/src/extension/notify.ts +31 -2
- package/src/extension/playbook.ts +6 -3
- package/src/extension/runtime.ts +882 -81
- package/src/extension/spawn-hints.ts +52 -12
- package/src/extension/summary.ts +7 -15
- package/src/extension/timing.ts +4 -15
- package/src/legion/db.ts +23 -1
- package/src/legion/events.ts +7 -0
- package/src/legion/interrupt.ts +98 -0
- package/src/legion/mail-release.ts +266 -0
- package/src/legion/mail.ts +456 -29
- package/src/legion/nodes.ts +31 -0
- package/src/legion/phase-conditions.ts +7 -1
- package/src/legion/phase-machine.ts +160 -8
- package/src/legion/state-machine.ts +34 -2
- package/src/runs/acceptance.ts +105 -6
- package/src/runs/args.ts +16 -0
- package/src/runs/child-scope.ts +323 -0
- package/src/runs/extension-inheritance.ts +145 -19
- package/src/runs/orchestrator.ts +613 -55
- package/src/runs/store.ts +149 -5
- package/src/shared/proc-sweep.ts +317 -0
- package/src/shared/progress.ts +11 -2
- package/src/shared/session-map.ts +29 -0
- package/src/shared/session.ts +308 -6
- package/src/shared/types.ts +34 -0
- package/src/supervisor/inprocess-supervisor.ts +158 -12
- package/src/supervisor/race-timeout.ts +55 -0
- package/src/supervisor/routing-supervisor.ts +55 -4
- package/src/supervisor/rpc-supervisor.ts +215 -29
- package/src/supervisor/types.ts +439 -2
- package/src/tui/status.ts +2 -1
- package/src/tui/viz/canvas.ts +358 -36
- package/src/tui/viz/palette.ts +18 -2
- package/src/tui/viz/panels.ts +507 -104
- package/src/tui/viz/rate.ts +120 -15
- package/src/tui/viz/view.ts +72 -4
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
| D3 | centurion 权力 = 汇总权 + 对**自己组员**的标准 steer/retire | 不越界管别人的队 |
|
|
38
38
|
| D4 | 越级通道(叶子直达 CEO)**不做** | v2 明确出清,见 §16 |
|
|
39
39
|
| D5 | 规模目标:数十 ~ 300 节点 | 决定 RPC headless 架构 |
|
|
40
|
-
| D6 |
|
|
40
|
+
| D6 | 树状交付:子树 squash 分支(有 remote 时附 MR/PR)→ 逐级上交,根统一合入默认分支 | 现有纪律的递归推广(REV-29) |
|
|
41
41
|
| D7 | 传输层换 **pi RPC 模式**(`RpcClient` 子进程),去 herdr 展示依赖 | 实测:`--mode rpc --session <file>` 可用、resume 无损 |
|
|
42
42
|
| D8 | worktree **仅支持 git**(裸 `git worktree add`,现有实现) | 不做 herdr worktree 分支 |
|
|
43
43
|
| D9 | resume 必须支持;父死 = 子死(stdin close),恢复走 resume 链 | F13 语义变化,已知并接受 |
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
interface LegionSupervisor {
|
|
88
88
|
spawnChild(input: SpawnInput): Promise<ChildHandle>; // RpcClient 启动 + 首个 prompt
|
|
89
89
|
prompt(name: string, text: string): Promise<void>;
|
|
90
|
-
steer(name: string, text: string): Promise<
|
|
90
|
+
steer(name: string, text: string): Promise<SteerDisposition>; // RPC steer:入队,回执三态之一(见下注)
|
|
91
91
|
followUp(name: string, text: string): Promise<void>; // 空闲后送达
|
|
92
|
-
abort(name: string): Promise<
|
|
92
|
+
abort(name: string, opts?: { clearQueued?: boolean; timeoutMs?: number }): Promise<AbortOutcome>; // 软中断:三态,见下注
|
|
93
93
|
waitSettled(name: string, timeoutMs?: number): Promise<SettleResult>; // 事件驱动,替代轮询
|
|
94
94
|
isAlive(name: string): boolean; // 替代 agentGet presence
|
|
95
95
|
stats(name: string): Promise<UsageSnapshot | null>; // get_session_stats → usage 表
|
|
@@ -97,6 +97,57 @@ interface LegionSupervisor {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
> **`SteerDisposition`(#313)**:`steer()` 返回**它到底做到了什么**,三态之一——`queued`(已入队,
|
|
101
|
+
> 等子节点本轮工具批结束后的 turn 边界)、`accepted`(子节点当时空闲,这一调用为它起了一轮,模型仍需
|
|
102
|
+
> 读)、`intercepted`(被 input handler 拦下,子节点从未看到)。**没有 `delivered`**:没有任何一层能
|
|
103
|
+
> 观察到模型真的读了这段文本,出现这种宣称必是编造。SDK 依据:`AgentSession._queueUserInput` 在
|
|
104
|
+
> `_queueSteer` **之前**就 `return "handled"`(`dist/core/agent-session.js:1694-1697`);
|
|
105
|
+
> `dist/modes/rpc/rpc-client.js:133` 同样注「disposition 为 `handled` 时这一 prompt 没有起任何一轮」。
|
|
106
|
+
|
|
107
|
+
> **`AbortOutcome`(#314)**:`abort()` 返回**它到底做到了什么**,同样是三态,没有第四种——
|
|
108
|
+
> `interrupted`(有一轮在跑,中断请求已送达;可能有部分编辑残留,但 child 仍活着、可续)、
|
|
109
|
+
> `no-turn`(原先没有一轮在跑,什么都没做,队列也不清)、`timeout`(请求发出去了,但 child 没在
|
|
110
|
+
> 上限内 settle;不杀、不 retire,回执指向 `stop`)。默认先清 child 的 steering / follow-up 队列
|
|
111
|
+
> (`clearQueued: true`),上限 `DEFAULT_ABORT_TIMEOUT_MS`(5 s)**同时盖住清队列与中断**。
|
|
112
|
+
> 契约与逐字回执:`docs/contracts/abort-soft-interrupt-v1.md`。
|
|
113
|
+
|
|
114
|
+
> **`capabilities.promptDisposition`(#323 D1)**:`prompt()` 到底能观察到什么,两个后端**不一样**,
|
|
115
|
+
> 所以能力上限写在接口上(可选成员 `capabilities?: { promptDisposition: boolean }`),而不是靠调用方猜:
|
|
116
|
+
>
|
|
117
|
+
> - **in-process**(默认 spawn 模式,`capabilities.promptDisposition === false`):SDK 的
|
|
118
|
+
> `AgentSession.prompt()` 返回 `void`,不交回 disposition,所以「被 input handler 吞掉」在这里
|
|
119
|
+
> **检测不到**。该后端上的 `accepted` **只表示这次调用没有抛错**——既不证明起了一轮,也不证明
|
|
120
|
+
> 模型读了。**调用方**禁止替 in-process 编造 `queued`/`intercepted`(不硬造一个它答不上来的
|
|
121
|
+
> 事实);后端自己按 `wasActive` 推断出的 `queued`(同一后端 `dispatchInContext` 在轮次已活跃
|
|
122
|
+
> 时给 `steer` 的回包)只算「**最多排队**」的**弱声明**——它说的是「这一调用没有立刻起一轮」,
|
|
123
|
+
> 不是「模型一定会在下个边界读到」。
|
|
124
|
+
> - **rpc**(`promptDisposition === true`):`prompt()` 的回包带 disposition,`accepted` 才有
|
|
125
|
+
> 「这一调用为它起了一轮」的强度(`intercepted` 由 `handled` 归一化而来,见上一条注)。
|
|
126
|
+
> - **`RoutingSupervisor`** 按子节点路由,所以 facade 上的标志只能声明「它可能路由到的**所有**
|
|
127
|
+
> 后端都能答的事实」。in-process 是默认模式且答案为 `false`,故 facade 报 `false`;逐后端的
|
|
128
|
+
> 精确值以 `RpcSupervisor.capabilities` / `InProcessSupervisor.capabilities` 为准。
|
|
129
|
+
> **facade 级读取不得用于区分后端**(这个词是交集,不是完整事实);需要区分时读具体后端
|
|
130
|
+
> 实例(`RoutingSupervisor.backendFor` 选中的那个,公开可见的是 `routing.rpc` /
|
|
131
|
+
> `routing.inprocess` 的 `capabilities`)。
|
|
132
|
+
>
|
|
133
|
+
> 同一条能力上限解释了 mailbox 的 `prompt` 分支为什么没有 `intercepted` 告警(#323 2):那条
|
|
134
|
+
> 分支的 `prompt()` 是 `Promise<void>`,没有 disposition 可读,它写下的 `ACCEPTED`
|
|
135
|
+
> (`delivered=1`,五值语义见 §8.3.1)只表示「send 调用返回了」,不是「模型读了」。
|
|
136
|
+
|
|
137
|
+
> **`steer` 的负语义(#313)**:§8.3 表格里 `steer` 那一行的「本轮工具后、下次 LLM 前送达」也是
|
|
138
|
+
> **排队**语义,不是打断。
|
|
139
|
+
> 工具调用执行期间子节点对 `steer` **不可达**——queue-only,没有自动打断,也没有 abort 后重投;
|
|
140
|
+
> 一个卡在长命令里的子节点永远收不到 steer,文本只在它回到 turn 边界时才被消费。要打断**当前这一
|
|
141
|
+
> 轮**用 `abort`(软中断:上下文保留、子节点仍可续,见 `docs/contracts/abort-soft-interrupt-v1.md`);
|
|
142
|
+
> `stop`(`force: true` 是硬版本)是拆掉进程那一条。父侧还有**二级排队**:父自己正卡在
|
|
143
|
+
> `await subagent({ action: "wait" })` 里时,用户的插话先变成**父自己的** steering 消息、同样等父的
|
|
144
|
+
> turn 边界(SDK:`AgentSession.prompt` 在 streaming 期间按 `streamingBehavior` 走
|
|
145
|
+
> `_queueSteer`/`_queueFollowUp`,`dist/core/agent-session.js:1549-1560`;`AgentSession.steer` 是同一
|
|
146
|
+
> 条队列,`:1708-1717`)。Esc 能让父**立刻**回到 turn 边界并处理它;否则要等该 `wait` 自己结束
|
|
147
|
+
> (返回或超时)才轮得到。而 Esc 只结束父的**这一轮**,**不回收**子节点:
|
|
148
|
+
> `src/extension/runtime.ts:1596` 的 `esc-no-retire-v3` 注释、`index.ts` 的 `INTERRUPTED_HEAD`
|
|
149
|
+
> (`interrupted (parent turn ended; children still running)`)、parent playbook 第 3 条。
|
|
150
|
+
|
|
100
151
|
`SpawnInput` 承载现有 `buildPiArgs` 全套 flag(`--session`/`--model`/`--tools`/`--extension`/`--append-system-prompt`),经 `RpcClient` 构造参数 `{cliPath, cwd, env, args}` 传入。**M1 验收项 a** 即验证此承载。
|
|
101
152
|
|
|
102
153
|
### 3.2 生命周期映射
|
|
@@ -129,10 +180,10 @@ interface LegionSupervisor {
|
|
|
129
180
|
- 连接约定:每 supervisor 进程一连接;`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;`;写入均为短事务 prepared statement
|
|
130
181
|
- **WAL 是前置条件,不是请求**:`PRAGMA journal_mode=WAL` 是**查询**,返回值才是实际生效的 journal mode,它可能在**不报错**的情况下降级(网络/FUSE 挂载无 shared-memory 支持、只读介质、争用锁)。`openLegionDb()` 因此**读取返回值并在其为文件库时校验**:非 `wal` 直接抛错,在启动期炸掉,而不是带着「已开 WAL」的假设继续跑、把问题拖成后发性数据事故。`:memory:` 是唯一的刻意豁免——SQLite 对内存库不实现 WAL、永远报 `memory`,而该库本来就不跨进程存活(由 `PRAGMA database_list` 的空 `file` 判定,不是靠路径字符串匹配)。测试见 `test/unit/legion-db.test.ts`。
|
|
131
182
|
|
|
132
|
-
### 4.2 Schema(`PRAGMA user_version =
|
|
183
|
+
### 4.2 Schema(`PRAGMA user_version = 2`)
|
|
133
184
|
|
|
134
185
|
```sql
|
|
135
|
-
PRAGMA user_version =
|
|
186
|
+
PRAGMA user_version = 2; -- v1→v2 无 DDL:只把 messages.delivered 收宽为五值语义(§8.3.1,REV-23)
|
|
136
187
|
|
|
137
188
|
CREATE TABLE nodes (
|
|
138
189
|
id TEXT PRIMARY KEY, -- 树形 id(§5)
|
|
@@ -148,7 +199,7 @@ CREATE TABLE nodes (
|
|
|
148
199
|
team TEXT,
|
|
149
200
|
status TEXT NOT NULL, -- starting|running|blocked|settled|failed|retired
|
|
150
201
|
phase TEXT, -- centurion 专有:planning|implementing|reviewing|verifying|done|failed|aborted
|
|
151
|
-
phase_since INTEGER, --
|
|
202
|
+
phase_since INTEGER, -- 当前相位 stint 的起始时刻(超时判定 + 通知过期判据;复活时相位不动而它前移,REV-26)
|
|
152
203
|
rework_count INTEGER NOT NULL DEFAULT 0,
|
|
153
204
|
contract_path TEXT, -- planning 产物(§7.2)
|
|
154
205
|
task_summary TEXT,
|
|
@@ -184,7 +235,7 @@ CREATE TABLE messages ( -- 邮件(§8)
|
|
|
184
235
|
body TEXT NOT NULL,
|
|
185
236
|
kind TEXT, -- question|report|handoff|notice(display-only,§10.4)
|
|
186
237
|
urgency TEXT NOT NULL DEFAULT 'info', -- info | action-needed
|
|
187
|
-
delivered INTEGER NOT NULL DEFAULT 0, -- 0=pending 1=
|
|
238
|
+
delivered INTEGER NOT NULL DEFAULT 0, -- 0=pending 1=accepted 2=bounced 3=delivered 4=obsolete(五值语义见 §8.3.1)
|
|
188
239
|
delivered_at INTEGER,
|
|
189
240
|
created_at INTEGER NOT NULL
|
|
190
241
|
);
|
|
@@ -194,7 +245,11 @@ CREATE INDEX idx_messages_rate ON messages(from_node, created_at); -- from_nod
|
|
|
194
245
|
|
|
195
246
|
### 4.3 事件类型枚举(闭集,新增需修订契约)
|
|
196
247
|
|
|
197
|
-
`node_launched` `node_settled` `node_failed` `node_retired` `node_resumed` `node_blocked` `phase_change` `phase_timeout` `mail_sent` `mail_delivered` `mail_bounced` `budget_refused` `orphan_detected` `tree_finalized`
|
|
248
|
+
`node_launched` `node_settled` `node_failed` `node_retired` `node_resumed` `node_blocked` `node_abandoned` `node_handled` `phase_change` `phase_timeout` `mail_sent` `mail_delivered` `mail_bounced` `budget_refused` `orphan_detected` `tree_finalized`
|
|
249
|
+
|
|
250
|
+
共 16 种,与 `src/legion/events.ts` 的 `LEGION_EVENT_TYPES` 逐项对齐:`node_abandoned` 由 hygiene 的
|
|
251
|
+
`markAbandoned` 写(#233),`node_handled` 是 #309 的失败处理审计(见 `docs/contracts/partial-failure-exit-v1.md`),
|
|
252
|
+
两者都**经契约修订**加入——闭集语义未变,写入方与读取方仍由同一份名单保证一致。
|
|
198
253
|
|
|
199
254
|
**事件粒度**:只记树级语义事件。工具调用级明细留在 session jsonl,不入 events(防低价值流水账)。
|
|
200
255
|
|
|
@@ -225,7 +280,7 @@ CREATE INDEX idx_messages_rate ON messages(from_node, created_at); -- from_nod
|
|
|
225
280
|
---
|
|
226
281
|
name: centurion
|
|
227
282
|
description: 百夫长/组长:携带一支团队递归派生的指挥角色。负责冻结契约、
|
|
228
|
-
调度组员、推进四阶段状态机、整队交付(squash MR)。
|
|
283
|
+
调度组员、推进四阶段状态机、整队交付(squash 分支;有 remote 时开 MR)。
|
|
229
284
|
kind: pi
|
|
230
285
|
allowNestedSubagents: true
|
|
231
286
|
maxSubagentDepth: 2 # R=本角色子树代数上限(含自身;与所在深度无关;只收紧不放宽):1=叶子禁派生;2=可派一层组员、组员不可再派;3=组员(如孙代 centurion)可再派一层
|
|
@@ -234,7 +289,7 @@ team: # 可选:默认队(须是 settings.teams 里真
|
|
|
234
289
|
---
|
|
235
290
|
```
|
|
236
291
|
|
|
237
|
-
角色正文要点(写给 LLM 的职责说明):只管自己队;planning 先出契约再开工;implementing 按文件所有权并行派工;reviewing 多角度对抗审查;`legion_phase` 请求迁移被拒时按拒绝理由修正后重试(不算 rework);收队时 squash
|
|
292
|
+
角色正文要点(写给 LLM 的职责说明):只管自己队;planning 先出契约再开工;implementing 按文件所有权并行派工;reviewing 多角度对抗审查;`legion_phase` 请求迁移被拒时按拒绝理由修正后重试(不算 rework);收队时 squash 到分支 + 汇总 verdict(有 remote 时附 MR/PR URL);组员卡死按现有止损纪律(重试≤2 → 咨询 advisor 或上报)。
|
|
238
293
|
|
|
239
294
|
### 6.2 团队绑定:角色 frontmatter `team` 为默认,launch 参数覆盖,经现有 `PI_SUBAGENTS_TEAM` env 通道下发——centurion 的 roster 即所选队的成员(现成机制,README:498)。
|
|
240
295
|
|
|
@@ -256,13 +311,13 @@ PLANNING → IMPLEMENTING → REVIEWING → VERIFYING → DONE
|
|
|
256
311
|
|---|---|---|
|
|
257
312
|
| →planning | 队创建 | 无 |
|
|
258
313
|
| planning→implementing | centurion 经 `legion_phase` 请求 | `contract_path` 存在且非空(契约已落盘) |
|
|
259
|
-
| implementing→reviewing | 同上 | 全部 worker 子节点 settled(failed
|
|
314
|
+
| implementing→reviewing | 同上 | 全部 worker 子节点 settled(failed 的须已处理,来源有三:rework 重派、`supersedes` 指名一个已被 settled 兄弟接手的失败、或转 failed) |
|
|
260
315
|
| reviewing→verifying | 同上 | ≥1 个 reviewer settled 且 verdict `ok:true` |
|
|
261
316
|
| reviewing→implementing | 同上(rework) | `rework_count += 1`;>2 拒绝并转 failed |
|
|
262
317
|
| verifying→implementing | 验收命令有红 | 同上(rework) |
|
|
263
318
|
| reviewing→planning | 同上(centurion 裁量:计划本身错了) | 允许,但**必须**写 `phase_change` 事件并 mail 通知上级知悉 |
|
|
264
319
|
| verifying→done | 验收全绿 | **supervisor 自动迁移,无 LLM 参与**(D14) |
|
|
265
|
-
| *→failed | rework 超限 / 组员全部失败 / 上级裁决 | 终态 |
|
|
320
|
+
| *→failed | rework 超限 / 组员全部失败 / 上级裁决 / centurion 声明判负(`legion_phase{to:"failed", reason}`,`terminalReason: parent_decision`) | 终态 |
|
|
266
321
|
| *→aborted | 上级 retire 整队 | 终态 |
|
|
267
322
|
|
|
268
323
|
不变量(违反即拒绝迁移 + 报警事件):implementing 中不允许存在 running 的 planner;DONE 时全部子节点必须终态;reviewing 至少一个 reviewer settled 过才可采信 verdict。
|
|
@@ -282,11 +337,11 @@ npm test -- --filter <scope>
|
|
|
282
337
|
|
|
283
338
|
### 7.3 verifying 执行:supervisor 在子树 worktree cwd 逐条执行 `legion-verify` 块内命令(`timeout` 包裹,默认 600s/条,可设),输出落 runDir + 事件记录。空块 = 直通 done(read-only 队常见)。
|
|
284
339
|
|
|
285
|
-
### 7.4 超时(每阶段独立时钟,默认可设,role/task 可覆盖):planning 15m / implementing 60m / reviewing 30m / verifying 15m
|
|
340
|
+
### 7.4 超时(每阶段独立时钟,默认可设,role/task 可覆盖):planning 15m / implementing 60m / reviewing 30m / verifying 15m。时钟来源依次为:显式 operator 设置(`subagents.legion.phaseTimeouts`)→ 团队卡片 `phases[].timeoutMs` → 内置四阶段默认(上述四值)→ 0(永不告警),相位名也可以是卡片自定义的名字(REV-26;内置四值不变)。超时**不自动杀**:写 `phase_timeout` 事件 + mail 通知该 centurion 的上级,由上级裁决(steer 续期 / abort 打断当前轮且 child 仍在 / retire 收队并进 aborted)。确定性优先,避免意外屠杀。
|
|
286
341
|
|
|
287
342
|
### 7.5 DONE 收队流(全在 centurion 进程内自动编排):
|
|
288
343
|
1. verifying 全绿 → supervisor 迁移 phase=done(事件)
|
|
289
|
-
2. 注入收队 prompt:汇总 verdict、squash
|
|
344
|
+
2. 注入收队 prompt:汇总 verdict、squash 到分支(有 remote 时开 MR)
|
|
290
345
|
3. centurion settle → 其父 collect → retire centurion
|
|
291
346
|
4. worktree 保留至根节点合 MR 后由 `legion-gc` 清理(文件不删库记录)
|
|
292
347
|
|
|
@@ -324,20 +379,58 @@ npm test -- --filter <scope>
|
|
|
324
379
|
| running + `info` | `follow_up` | 排队等空闲 |
|
|
325
380
|
| idle/settled 但进程在 | `prompt` | 直接开新 turn |
|
|
326
381
|
| retired 且非终态 | **resume 后投递**(D9:`--session` 唤醒,首条消息即此信) | 邮件即召唤令 |
|
|
327
|
-
| 终态(done/failed/aborted) |
|
|
382
|
+
| 终态(done/failed/aborted) | 退信(`delivered=2` BOUNCED)+ 回执邮件给发送方 | — |
|
|
383
|
+
|
|
384
|
+
> **`steer` 行的负语义(#313)**:`steer` 只是排队——收件人正在跑工具调用时**收不到任何东西**,
|
|
385
|
+
> 没有自动打断,也没有 abort 后重投,消息只在下一个 turn 边界被消费;投递方拿到的三态回执
|
|
386
|
+
> (`queued`/`accepted`/`intercepted`)见 §3.1,它们落库后的取值见 §8.3.1。父侧二级排队同理:父在
|
|
387
|
+
> `await subagent({ action: "wait" })` 里时,用户插话先成为**父自己的** steering 消息、等父的 turn
|
|
388
|
+
> 边界(SDK:`dist/core/agent-session.js:1549-1560` 的 streaming 排队路径);Esc 能让父**立刻**回到
|
|
389
|
+
> turn 边界并处理它,否则要等该 `wait` 自己结束(返回或超时)才轮得到。Esc 只结束父的这一轮,
|
|
390
|
+
> **不回收**子节点(§3.1 注;`src/extension/runtime.ts:1596`;parent playbook 第 3 条)。
|
|
328
391
|
|
|
329
392
|
根节点收件箱 → `pi.sendMessage` 注入(复用现有 notify 机制)。**异步无等待**:发送即返回;刻意不做同步请求-响应(两个 LLM 互等 = 死锁 + token 焚烧炉)。
|
|
330
393
|
|
|
394
|
+
#### 8.3.1 投递状态五值(`messages.delivered`,schema v2,#322)
|
|
395
|
+
|
|
396
|
+
`delivered` 是**语义约定**而不仅是列:列本身还是 `INTEGER NOT NULL DEFAULT 0`,域由写入方/读取方共同遵守(`src/legion/mail.ts` 的 `MAIL_DELIVERY`)。
|
|
397
|
+
|
|
398
|
+
| 值 | 名称 | 谁能写 | 含义 |
|
|
399
|
+
|---|---|---|---|
|
|
400
|
+
| 0 | `PENDING` | 默认值 | 尚未交给收件人;`pendingInbox` 只返回这一档,也是唯一值得重试的档 |
|
|
401
|
+
| 1 | `ACCEPTED` | 邮差(send 返回) | **send 调用返回了**(`queued` / `accepted`)——不声称被读取;旧名 `INJECTED` 仅为兼容保留的 `@deprecated` 别名,同值 1 |
|
|
402
|
+
| 2 | `BOUNCED` | 邮差 | 永不送达:收件人终态,或被 input handler 拦下(`intercepted`,`reason: "intercepted_by_input_handler"`);发送方收到 `mail_bounced` 事件 + 退信邮件 |
|
|
403
|
+
| 3 | `DELIVERED` | 邮差 tick 确认 | 收件人**真的消费了**(判据见下);`mail_delivered` 事件保留原 type |
|
|
404
|
+
| 4 | `OBSOLETE` | 邮差 tick 确认 / 投递前校验 | 曾经有效,但送达前已失去意义(#333);写 `mail_bounced` 事件 + `reason: "obsolete_after_phase_change"`(**不新增事件类型**) |
|
|
405
|
+
|
|
406
|
+
合法迁移(其余一律 `InvalidDeliveryTransitionError`,同值重复为幂等空操作):`0→1`、`0→2`、`0→4`、`1→3`、`1→4`。
|
|
407
|
+
`1→3` 之后不再回退,`2/3/4` 是终值。写入 `1` 时若收件人已是终态,按 `0→2` 落 `BOUNCED`。
|
|
408
|
+
|
|
409
|
+
**`DELIVERED` 判据(冻结)**:解析收件人 session 文件(`parseSessionFile`),其**最后 20 个 turn** 的 user 文本中存在一条与**邮差实际注入的文本**(`formatMail(message)`:含 `[legion mail #id]` 信封)**完全相等**的字符串。
|
|
410
|
+
只认**注入信封**这一种形式(#338 M2):原始 body 不带 message id,任务卡 / 另一封信 / 被引用的原文都能与它整串相等,于是「别处出现过同一句话」会被误读成「这封已送达」。
|
|
411
|
+
刻意用精确相等而非 contains/前缀/去空白/模糊匹配:子节点在报告与 verdict 里会大量引用邮件原文,任何包含式判定都会把「提到过」误读成「看到过」,而这个状态的唯一职责就是把「子节点到底看到没有」这件事答成事实。
|
|
412
|
+
确认扫描每 tick 最多 N=20 封,且 **per-seam 游标轮转**(取 `id > cursor` 的下一批,扫到尾回绕):某收件箱 M 封 `ACCEPTED` 时一封的确认最坏晚 `ceil(M / N)` 个 tick,而不是被前 20 封永久占住窗口(#338 M3)。
|
|
413
|
+
|
|
414
|
+
**`OBSOLETE` 判据(冻结,#333)**:仅 `kind === "notice"` 且 `subject` 以 `Phase timeout:` 开头的邮件可被作废(kind 闭集是 `question|report|handoff|notice`,**没有** `instruction`;其它 kind **永不**自动作废——一个问题 / 一份交接过不过期由人判,#338 M4)。
|
|
415
|
+
“该 phase” 取自产生该通知的 `phase_timeout` 事件(`data.phase`/`data.phaseSince`),**不解析正文**:发件节点当前 `phase`/`phase_since` 与之不同(含之后重新进入同一 phase)或**已结束** ⇒ 已过期。
|
|
416
|
+
**复活 = 新 stint(#357/REV-26)**:`continue` 唤醒已结束团队时 `phase_since` 前移到复活时刻(相位不变),产生旧通知的 `phase_timeout` 事件与当前 `phase_since` 因而不同 ⇒ 旧通知判 `OBSOLETE`,而复活后的 stint 拥有自己的去重键,可再次告警。
|
|
417
|
+
**「已结束」的判据是 `hasEnded`(#354)**:终态(`isTerminalNode`)**或** `status` 为 `settled`/`retired`。一个只做调查、从不调用 `legion_phase` 的团队,结局时 `phase`/`phase_since` 停在出生值,于是旧判据(只看相位)认为通知仍有效——这正是 #354 的形状。`reason` 文案不变(仍 `obsolete_after_phase_change`,事件闭集不动),只是覆盖面明确包含「在超时相位里就地结束」;`isTerminalNode` 本身**不**放宽(投递退信与孤儿扫描共用它)。
|
|
418
|
+
求值两处:**投递前**(`PENDING→OBSOLETE`,不再注入,这是 #333 的根因路径)与 **tick 确认**(`ACCEPTED→OBSOLETE`,只改标签不回滚已注入的文本)。`DELIVERED` 优先于 `OBSOLETE`:确实消费过的事实不会被改写成“失效”。
|
|
419
|
+
|
|
420
|
+
**后端能力上限(`prompt` 分支)**:in-process 后端的 `prompt` 返回 `void`,无 disposition 可读,**无法区分 `queued` 与 `accepted`**,因此这一分支落 `ACCEPTED` 是能力上限而非观测结论;同理 `resumeChild` 首条消息即此信,若被包装成任务卡文本,精确匹配会落空而停在 `ACCEPTED`。
|
|
421
|
+
**root 收件人的证据来源(#338 M1)**:root 行在创建时(`createRootLegion`)写入本进程 `ctx.sessionManager.getSessionFile()`,因此发给 root 的邮件(含 #333 的 `Phase timeout:` 路径)可以升级到 `DELIVERED`;但 `run.json` 不记录 root 会话文件,所以 `rebuildLegionDb` 重建出的账本里该列为 NULL,这些邮件停在 `ACCEPTED`(不误判)。
|
|
422
|
+
|
|
423
|
+
|
|
331
424
|
### 8.4 工具签名(注册给所有子节点,与 `allowNestedSubagents` 无关)
|
|
332
425
|
|
|
333
426
|
```ts
|
|
334
427
|
legion_mail({ to, subject, body, kind?, urgency? }) // to: 短名|node id|parent|squad|children
|
|
335
428
|
// body 软上限 4KB(超限警告建议走文件),硬上限 32KB 拒发
|
|
336
429
|
legion_tree({ scope?: "children"|"squad"|"subtree"|"tree", depth? }) // 只读节点行摘要
|
|
337
|
-
legion_phase({ to, note?, evidence? })
|
|
430
|
+
legion_phase({ to, note?, evidence?, reason?, supersedes? }) // 仅 centurion(§7)
|
|
338
431
|
```
|
|
339
432
|
|
|
340
|
-
### 8.5 护栏:每节点发送速率默认 10 封/5
|
|
433
|
+
### 8.5 护栏:每节点发送速率默认 10 封/5 分钟(超限拒发);投递落值按 §8.3.1 的五值语义——**send 返回即 `ACCEPTED(1)`**(不再声称投递成功)、`intercepted` 落 `BOUNCED(2)`、消费证据落 `DELIVERED(3)`、失效落 `OBSOLETE(4)`;`pendingInbox` 只读 `PENDING(0)`(崩溃恢复最坏重复投递,信带 id 可去重——dup 可忍,丢不可忍);速率/上限进 settings(§11.4)。只读查询面 `mailboxStats(db, nodeId): { pending, accepted, delivered, bounced, obsolete }`(目前无消费者:D5 只要求导出 + 单测,接线另议,#338 minor 4)。
|
|
341
434
|
|
|
342
435
|
## 9. 隔离与交付(子树 worktree、MR 树)
|
|
343
436
|
|
|
@@ -345,7 +438,7 @@ legion_phase({ to, note?, evidence? }) // 仅 centurion(§7)
|
|
|
345
438
|
- **worktree 仅 centurion 可用**(REV-17,#214):`worktree: true` 是 centurion 角色的专属权限,不存在其他持有者。判定以角色名 `centurion` 为准:centurion 卡一律拿到子树 worktree(嵌套 centurion 拿 nested worktree,不受「父是 centurion ⇒ 组员不隔离」压制);centurion 的组员一律不拿;其余角色(`worker` / `prototype` / `scout` / `reviewer` / `planner` / `advisor` / `designer`,含用户自建 writer 角色)既不自动获得、也不会因 `acceptance.role: writer` 而获得 worktree
|
|
346
439
|
- **违规处理为降级,不是报错**(REV-17):非 centurion 角色卡声明 `worktree: true`、或 `subagent({ worktree: true })` 显式传参,一律**降级为 `worktree: false`**,并 `console.warn` 一次(每 Orchestrator 实例一次,同 `depthWarningEmitted` 语义)。**不抛错**——抛错会阻断 launch,而降级的语义是「回到契约本来的边界」;被降级的组员与父共享 checkout,其改动留在父的工作区里
|
|
347
440
|
- 孙代 centurion 在子树 worktree 内再开 nested worktree(git 原生支持),递归成立
|
|
348
|
-
-
|
|
441
|
+
- **交付树**(D6,REV-29):每棵子树一个 squash 分支(有 remote 时附 MR/PR)→ 父节点(或其队)合并 → 逐级上交 → 根合入默认分支。合并权在各级父节点,叶子永不 merge;合并认的是分支/提交,不是平台对象(现有纪律的递归推广)
|
|
349
442
|
- 文件所有权清单随契约(§7.2)逐层传递;契约矛盾 = 正确拒绝,上报修订后重派
|
|
350
443
|
|
|
351
444
|
## 10. 可视化与事件总线
|
|
@@ -366,7 +459,7 @@ interface LegionEventBus {
|
|
|
366
459
|
}
|
|
367
460
|
```
|
|
368
461
|
|
|
369
|
-
事件类型分两域(REV-13 明确):**存储域**(对齐 §4.3 的
|
|
462
|
+
事件类型分两域(REV-13 明确):**存储域**(对齐 §4.3 的 16 种,经 createDbEventSource 脏检查入总线)与**进程内域**(node_launched/node_steered/node_retired/node_aborted/ui_request + 细粒度观察流 text_delta/tool_call/turn_start/agent_settled,由扩展层在调用 supervisor 方法与接收 uiProxy 回调时同步 emit;事件带 nodeId,不落库)。消费端(面板/树视图/浮窗/未来动画)只订阅总线,永不直连 DB 或进程事件。**喂料端可插拔**:
|
|
370
463
|
- 进程内直发(supervisor 执行命令/投递邮件的瞬间,方法调用级零延迟——覆盖"主干戏份":直接子节点全部活动)
|
|
371
464
|
- 跨进程增量读:UI 进程每 200–500ms `PRAGMA data_version` 脏检查(一次整数比较,实测可用)→ 变了才 `SELECT * FROM events WHERE id > <游标>`(append-only + AUTOINCREMENT 天然游标)
|
|
372
465
|
- **禁止** `fs.watch` 盯 db 文件(WAL 写入不触主文件 mtime,必漏)
|
|
@@ -439,7 +532,7 @@ interface LegionEventBus {
|
|
|
439
532
|
### M2 军团树(组织层:db + centurion + 状态机 + 邮件)
|
|
440
533
|
|
|
441
534
|
- **归属**:新增 `src/legion/{db,nodes,events,usage,mail,state-machine,budget,rebuild}.ts`、`agents/centurion.md`、`src/extension/legion-tools.ts`;修改 `orchestrator.ts`(launch 落库/树命名/预算查库/子树 worktree——经 REV-17 收为 centurion 专属,非 centurion 请求 warns 一次后降级)、`src/runs/worktree.ts`(subtree 模式)、`src/runs/args.ts`(env 增 `PI_LEGION_DB`/`PI_LEGION_NODE_ID`)、`src/agents/agents.ts`、`index.ts`(队级 resume、collect 落库)
|
|
442
|
-
- **验收**:(a) 三层树 e2e:CEO→centurion→workers×2 全阶段走通(契约落盘→并行实施→review verdict→verify 实跑→done 收队 + squash
|
|
535
|
+
- **验收**:(a) 三层树 e2e:CEO→centurion→workers×2 全阶段走通(契约落盘→并行实施→review verdict→verify 实跑→done 收队 + squash 分支);(b) rework 循环:review major→回 implementing→复审通过;(c) rework≥2 → failed 上抛;(d) kill centurion 进程 → 孤儿报警 → resume 恢复整队、settled 组员不复活、运行中组员上下文延续;(e) `maxActiveNodes` 超限拒派 + 事件;(f) 邮件:同级/父子/父←子通、跨子树拒发并提示路由、retired 收件人唤醒投递、终态收件人退信;(g) WAL 压测:60 模拟 supervisor 并发写 5 分钟,busy 超时率 <0.1%(**实测 2026-10-22 通过**:60 个 `fork` 子进程、各持一连接、共享一库,postman 节奏每 ~2s 一短事务,300s 内 9004 次尝试 / 9004 提交 / 0 次 busy = 0.000%;成功事务 p50 1.03ms / p95 1.99ms / p99 4.92ms。复跑:`npm run test:wal-stress`;CI 快速守卫 `test/unit/wal-concurrency.test.ts` 只钉住 busy_timeout 生效、WAL/超时前置条件与零 busy,**不证明** 5 分钟跨进程结论);(h) `legion-rebuild` 从 run.json+jsonl 重建 nodes/events;(i) 非 pi kind 明确拒绝
|
|
443
536
|
|
|
444
537
|
### M3 可视化
|
|
445
538
|
|
|
@@ -487,3 +580,12 @@ interface LegionEventBus {
|
|
|
487
580
|
| 2026-10-19 | REV-18 | v4 收紧为仅 kind: pi;AGENT_KINDS 收敛为 ["pi"];非 pi 值忽略并按 pi 启动。字段不删(值域收窄);`spawnMode` 的 `rpc`/`inprocess` 不受影响。契约:`docs/contracts/drop-non-pi-kinds-v4.md`(D1,v4.0.0) |
|
|
488
581
|
| 2026-10-21 | REV-19 | §4.2 新增索引 `idx_messages_rate ON messages(from_node, created_at)`(schema 变更,按 §15-4 走修订)。原因:`sendMail` 的限速 COUNT(`src/legion/mail.ts`,`WHERE from_node = ? AND created_at > ?`)在**写事务内**执行且 `messages` 无任何保留策略(`DELETE FROM messages` 仅见于 `rebuild.ts` 且只在 `preserveMessages === false`),实测 50k 行时由 `SCAN messages` 变为覆盖索引 `SEARCH`,成本随运行时长单调劣化。零行为变更:COUNT 结果不变,仅查询计划改变。**不改 `user_version`**(仍为 1),因此**既有 legion.db 不会获得该索引**——`migrate()` 在 `user_version === 1` 时提前返回,`SCHEMA_V1` 只在版本 0 执行。**且 `legion-rebuild` 不能修复它**:`rebuildLegionDb()` 拿的是已打开的 connection,事务内只发 `DELETE FROM usage/events/nodes`(以及 `preserveMessages === false` 时的 `DELETE FROM messages`),**没有 DROP/CREATE TABLE,也不重跑 `SCHEMA_V1`**;行删光后索引集合原封不动。实测:旧账本跑完 `rebuildLegionDb()` 与 close+reopen 后均仍为 `["idx_messages_inbox"]`,只有**删掉 legion.db 文件后重新打开**(走版本 0 建库)才会得到 `idx_messages_rate`。故补救方式**仅限重建账本文件本身**(legion.db 是可丢弃的树级索引,§4.4:从 run.json + session jsonl 重新生成),不是跑 rebuild 命令。接受此代价:每棵树各自新建 legion.db,长跑账本才需要人工删库重启。代价度量(50k 行表,200 发送者):限速 COUNT 由 `SCAN` 的 ~3327 µs/call 降至 ~6.6 µs/call(**~500x**,该量级稳定复现);写侧索引维护在已开事务内的单行 INSERT 上是**零点几 µs/行量级(低双位数百分比)**,方向确定但**具体倍数机器相关且不可移植**(同一代码在不同环境实测 +32% 到 +47% 不等,勿当常数引用);一次**含 COMMIT 的**单行写入被 fsync 主导(数百 µs、高方差),索引那零点几 µs 在该噪声内不可分辨。净效应每封信仍是 ms 量级的节省,且随行数增长而扩大。不新增「打开时补建索引」后门(会让契约漂移)。不引入 `messages` 保留策略(另立产品决策:归属、是否借用 `eventRetentionDays`、bounce 风暴是否单独限流)。触发:issue #237 P9 |
|
|
489
582
|
| 2026-10-22 | REV-20 | ① §4.1 显式写下 WAL **前置条件**:`PRAGMA journal_mode=WAL` 是查询、返回值才是实际生效模式,可在不报错的情况下降级,故 `openLegionDb()` 校验返回值、文件库非 `wal` 即抛错(`:memory:` 刻意豁免,由 `PRAGMA database_list` 的空 `file` 判定)。② §4.4 补「可重建」的**实测边界**:phase/phase_since/rework_count/contract_path/task_summary 全部不可恢复(`NodeSource` 无这些字段);events 塌缩为每节点一条合成事件(phase_change/mail_sent/budget_refused/tree_finalized 等零幸存);`messages` 无任何重建路径(INSERT 仅存在于 `mail.ts`,`rebuild.ts` 只有条件 DELETE)。按 §15-4 走修订的理由:②是**订正一条会误导持久性推理的既有断言**(原文「可重建」被当成放松丢写的前提),①是把一句被隐含的实现假设升为契约明文。非 schema/事件类型/权限变更,`user_version` 不变;**未触碰** `synchronous` / `journal_mode` 的取值 / `temp_store` / `cache_size` / `mmap_size`(`synchronous=NORMAL` 提案已评估**否决**:生产写速率 0.53–5.47 tx/s,`FULL` 占空比 0.027–0.273%,而 `NORMAL` 会把掉电窗口从单事务放大到上个 checkpoint 以来的全部事务、且长活读者下无界)。触发:issue #236 **注(对 REV-19 措辞的澄清)**:REV-19 原文写「legion.db 是可丢弃的树级索引」,该断言的语境**仅是**「旧账本拿不到新索引、补救只能删库重开」这一 schema 索引场景,并非「账本可随意丢弃」。本修订不追改 REV-19 的历史正文(修订日志按 §15-1 追加、保留当时的实际决定),而在此澄清其适用范围:可丢弃的**只是索引层**,`messages` 与 phase 状态断点不在重建覆盖内(见 §4.4)。 |
|
|
583
|
+
| 2026-10-30 | REV-21 | #314 abort(软中断)落地后的接口面同步(非 schema / 事件类型 / 权限变更,按 §15-1 追加):① §3.1 `abort` 缝由 `abort(name): Promise<void>` 收成 `abort(name, opts?: { clearQueued?: boolean; timeoutMs?: number }): Promise<AbortOutcome>`,并补 `AbortOutcome` 三态注——`interrupted`(有一轮在跑、中断请求已送达;可能有部分编辑残留,child 仍活着、可续)/ `no-turn`(原先没有一轮在跑,什么都没做,队列也不清)/ `timeout`(请求发出去了,但 child 没在上限内 settle;不杀、不 retire,回执指向 `stop`),默认 `clearQueued: true`,上限 `DEFAULT_ABORT_TIMEOUT_MS`(5 s)**同时盖住清队列与中断**;契约与逐字回执在 `docs/contracts/abort-soft-interrupt-v1.md`。② §3.1 的 `steer` 负语义段(即对 §8.3 表格 `steer` 行的注)补 abort/stop 分工:要打断卡在工具调用里的子节点的**当前这一轮**用 `abort`(软中断:上下文保留、子节点仍在、可续),拆掉进程那一条才是 `stop`(`force: true` 为硬版本)——原文此处只指向 `stop`,与已落地的 abort/stop 切分矛盾。③ §7.4 阶段超时的上级裁决括号由「steer 续期 / abort 收队」改为「steer 续期 / abort 打断当前轮且 child 仍在 / retire 收队并进 aborted」——「收队」对应 `retire`,`abort` 不回收 child,原文的 `abort 收队` 会让人以为 abort 会终结子节点。原因:issue #314 把 supervisor 里早已实现却零调用点的 `abort` 接到工具面(`SubagentParams.action` 增 `abort`),§3.1 的接口表与 §7.4 的收队语义必须与新 seam 同形,否则同一份契约里两处各说一套。`node_aborted` 事件类型与 payload `{ name }` 未变(进程内域仍按 REV-13 的闭集),不新增事件类型、不改 `user_version`。 |
|
|
584
|
+
| 2026-10-30 | REV-22 | §4.3 事件闭集由 14 补到 **16**(事件类型变更,按 §15-4 走修订):补记 `node_abandoned`(hygiene 的 `markAbandoned` 写,#233 修复时已进 `src/legion/events.ts` 的 `LEGION_EVENT_TYPES`,但 §4.3 清单漏记)与本 PR(#309)新增的 `node_handled`(`requestLegionPhase` 写;团队用 `supersedes` 认领一个失败 child 时的审计事件,语义见 `docs/contracts/partial-failure-exit-v1.md`)。同一次修订顺带对齐:§10.2 的条数(14→16)、§7.1 的 `implementing→reviewing` 进入条件(「failed 须已处理」补第三来源 `supersedes`)与 `*→failed` 触发(补「centurion 声明判负 `reason`」)、§8.4 的 `legion_phase` 签名(补 `reason?` / `supersedes?`),以及 `test/unit/legion-schema-sentinel.test.ts` 的 `EXPECTED_EVENTS`(该哨兵此前把 §4.3 的 14 名钉住,正是它让清单无法自行追上代码)。闭集语义未变:写入方与读取方仍由同一份 `LEGION_EVENT_TYPES` 保证一致,`mapEvent` 对未知存量类型仍抛错(#233 的游标冻结模式没有被重新引入);不改 `user_version`、不改 schema、不改 `LEGION_PROCESS_EVENT_TYPES`(仍 11 条)。触发:PR #332 对抗式 review 的 minor-3/4(文档漂移)——原评审把它记成「既有漂移」,但 §4.3 的那条清单是**被哨兵钉死的冻结契约**,所以补记必须走修订而不是改清单了事。 |
|
|
585
|
+
| 2026-11-01 | REV-23 | `messages.delivered` 由三值(0/1/2)收宽为**五值**(0 pending / 1 accepted / 2 bounced / 3 delivered / 4 obsolete),`LEGION_DB_USER_VERSION` 1→2,§4.2 的 `PRAGMA user_version` 随之改为 2(**schema 版本变更,按 §15-4 走修订**;但**无 DDL**——列仍是 `INTEGER NOT NULL DEFAULT 0`,五值域是写入方与读取方之间的语义约定,旧行的 `1` 直接按 `accepted` 解释、无需回填;旧账本也不会因此拿到 `idx_messages_rate`,同 REV-19)。同时补 §8.3.1(五值语义 + 迁移边 + `DELIVERED`/`OBSOLETE` 判据 + `prompt` 分支能力上限)并改写 §8.5(不再有「投递成功落 `delivered=1`」这句话)。语义变更要点:`intercepted` 从 `INJECTED + console.warn` 改为 `BOUNCED`(`reason: intercepted_by_input_handler`);新增 `DELIVERED`(session 最后 20 个 turn 里与注入文本**完全相等**才算);新增 `OBSOLETE`(仅对 `Phase timeout:` notice,且只在其 phase 已结束/节点已终态时)。**事件闭集不变(仍 16 种)**:`OBSOLETE` 复用 `mail_bounced` 并带 `reason: obsolete_after_phase_change`,`DELIVERED` 复用 `mail_delivered`,`ACCEPTED` 不写新事件(`sendMail` 的 `mail_sent` 已是该事实)。子契约:`docs/contracts/mailbox-delivery-v1.md`。触发:issue #322(信箱缺 accepted/delivered/bounced 三态)与 #333(相位超时 action-needed 到达即失效)。 |
|
|
586
|
+
| 2026-11-02 | REV-24 | §8.3.1 按 #338 的对抗式 review 收紧(**语义修订,非 schema / 非事件类型变更**,按 §15-1 追加):① `DELIVERED` 判据去掉「或与邮件 **body** 完全相等」这一分支,只认注入信封(`formatMail`)——body 不带 message id,任务卡/别封信/被引用原文都能与它整串相等,误判面过大;② 确认扫描由「每 tick 取最老 20 封」改为 **per-seam 游标轮转**(`id > cursor`,扫到尾回绕)——旧写法下 20 封无法判定的 `ACCEPTED` 会永久占住窗口,确认与作废两条通道一起停摆(死锁,不是延迟);③ `OBSOLETE` 条款里的 `kind === "instruction"` 更正为「只有 `kind === "notice"` 可作废」,kind 闭集仍是 `question|report|handoff|notice`(`instruction` 从来不存在);④ 补 root 收件人的证据来源与能力上限:`createRootLegion` 建 root 行时写入本进程会话文件,使发给 root 的邮件可确认到 `DELIVERED`;`run.json` 不记录该文件,故 `rebuildLegionDb` 重建的账本里仍停在 `ACCEPTED`;⑤ §8.5 的 `mailboxStats` 改为「导出 + 单测,目前无消费者」,不再声称已接 `/legion` 状态与 doctor。触发:PR #338 review(4 major + 6 minor)。 |
|
|
587
|
+
| 2026-11-02 | REV-25 | §8.3.1 OBSOLETE 判据:「该团队已结束」由 `isTerminalNode`(failed / done / failed / aborted 相位)放宽为 `hasEnded`(上者 ∪ `status ∈ {settled, retired}`)。原因:在超时那个相位里就地收队的团队(调查型团队、或在 `reviewing` 内完成返工的团队)此前的通知会在收队后仍投递(#354,实测 3/3 命中)。事件闭集不变(16),`reason` 文案仍 `obsolete_after_phase_change`,`isTerminalNode` 不放宽。 |
|
|
588
|
+
| 2026-11-02 | REV-26 | 三条**语义修订**(非 schema / 非事件类型 / 非权限变更,按 §15-1 追加)。① **§4.2 `phase_since` 的含义收窄为「当前相位 *stint* 的起始时刻」**:#357 之前它只能是「当前相位的起始时刻」,因为相位一迁才写;`continue` 复活一个已结束的团队时相位**不动**(工作从原地继续)而 `phase_since` 前移到复活时刻,好让「一个 stint 一次 `phase_timeout`」的去重键(`hasPhaseTimeoutForSince`)与 `Phase timeout:` 通知的过期判据都按新 stint 计算——否则一个超时过的团队复活后永远静默。② **§8.3.1 的 `OBSOLETE` 触发多一条来源:复活即令旧 stint 的通知作废**——判据仍是「`phase`/`phase_since` 与产生该通知的 `phase_timeout` 事件不同」(复活前移了 `phase_since`),`reason` 文案仍 `obsolete_after_phase_change`,事件闭集仍 **16**,未新增 event type / mail 主题前缀。③ **§7.4 的时钟来源扩为四级**:显式 operator 设置(`subagents.legion.phaseTimeouts`,只认**显式写下**的值)→ 卡片 `phases[].timeoutMs` → 内置四阶段默认 → 0(永不告警);相位名不再要求是内置四名(声明式状态机 §7.1),内置四阶段的**数值逐字不变**(planning 15m / implementing 60m / reviewing 30m / verifying 15m),超时仍只报告、永不杀。**④ 行为变更(同一 PR 的 major 1 修复,随本修订一并记录)**:此前 `index.ts` 把 `DEFAULT_LEGION_SETTINGS` 的 `phaseTimeouts` 展开成普通对象,于是「设置项已存在」与「operator 显式设置过」不可区分,`settings.phaseTimeouts[phase] ?? machineTimeout(...)` 的左侧永远非空——卡片为**内置相位名**声明的 `timeoutMs` 被静默忽略(探针:`implementing` 设置项 3600000 / 卡片 300000 → 实际 3600000)。现在显式性由 `legionExplicitFields` 判定(`legionSettingsForRun` 把默认值填进来时不再冒充决定),因此**声明了内置名 `timeoutMs` 的卡片从此真的生效**:这是本修订里唯一可观察的行为变更,也是声明式状态机 v5 契约 `docs/contracts/declarative-phase-machine-v5.md` 早已写下的顺序第一次与行为一致。触发:PR #363 对抗式 review 的 major 1/2(契约漂移:改动已落库而修订日志未追加,同 #355 的形状)。 |
|
|
589
|
+
| 2026-11-02 | REV-27 | **REV-1(子契约 `docs/contracts/centurion-declared-phases-v1.md` §4,子契约内编号;本文件编号顺延为 REV-27,避免与 2026-10-11 的 REV-1 撞号)**:把「非 root 不得派 centurion」从**未记录的实现约束**提升为契约条款(权限变更,按 §15-4 走修订)。该约束与 §9「孙代 centurion 在子树 worktree 内再开 nested worktree,递归成立」及 §6.1 `maxSubagentDepth` 注释「3=组员(如孙代 centurion)可再派一层」**直接冲突**,而此前没有任何 REV 记录。裁决:**保持禁止**。理由(老板决定,2026-10-31):centurion 的语义是「root 派出去的一支子树」,嵌套 lead 不带来新的交付语义,只带来深度与工件的递归;需要更多并行时由 root 直派多个 centurion。落地:`Orchestrator.launch` 在创建 worktree/run 目录**之前**抛 `centurion is dispatchable only from the root shell; '<parent>' at '<node>' cannot`(code 仍 `BUDGET_EXCEEDED`,与 #281 一致);`agents/centurion.md` 的 description 同步为「仅根会话可派发」。同一修订确立**声明式启用**:状态机不再是「角色叫 centurion」的自动后果,而是 root 在那次派发里显式声明的东西(`subagent({ agent: "centurion", phases: [...] })`)——**声明了 = §7.1 原行为一字不改**;**没声明 = 该节点 `nodes.phase` 为 NULL、不注册 `legion_phase`、不注入收队提示**,干完活交 verdict 即结束(§7.1 的「团队生来在 `planning`」由此变为「声明了才生来在机器声明的 start 相位」)。同一次修订还补两条守卫:`phases` 只能由 root 声明、且只能给 centurion(非 root 传 / 给非 centurion 传一律在 launch 期拒绝,不产生任何工件)。**worktree 归属不变**(§9 REV-17 保持:centurion 专属、强制 `true`、仍绑 rpc);不新增角色、不动 `teams` 裁剪、不放宽 MR 合并权(仍在各级父节点)。**另订正一处子契约与实现的冲突**:`centurion-declared-phases-v1.md` §5 的 I1 守卫列写「`test/unit/legion-tools.test.ts:114` / `test/integration/legion-tree.test.ts:164` 两个文件不改」,子契约 §6-2 又要求 G2/G3 断言「worktree 目录不存在」,但 M2 那条 e2e 必须改成**显式声明内置机器**(原来自动获得的状态机现在要写明)——这两处对应的都只是**测试**,不改任何被冻结的行为语义,M2 的断言除新增 `phases:` 外一行未动,I1 的「声明后与今天逐字节一致」正是由这条改动后的用例来证明。同理 `legion-phase-machine-start.test.ts` 里「未声明团队生来在 `planning`」的旧断言必须反转成 NULL(那条旧断言正是本契约要推翻的规则本身)。**并订正 §6-2 的一处漂移**:G2/G3 的「无工件」判据从「worktree 目录不存在」改为「run store 无新 child / 账本无新行」——非 centurion 在守卫之前就被强制 `worktree:false`,那条断言恒真(PR #371 review r1 minor 2);G4 仍保留 worktree 断言(它派的是真 centurion)。 |
|
|
590
|
+
| 2026-11-03 | REV-28 | 会话中断与恢复语义(**语义修订,非 schema / 非事件类型 / 非权限变更**,按 §15-1 追加;子契约 `docs/contracts/legion-session-continuity-v1.md`)。① **§3.1 的 retire 语义分叉**:`retire(cause: "session-shutdown")` **不再**迁移相位——父会话退出是**暂停**,不是判负。此前它走 `abortLegionPhase`,把 `nodes.phase` 推到终态 `aborted`,而 `childFromNode`/`reserveNode` 对终态相位直接抛 `CHILD_TERMINAL`,于是「结束一个会话」这个动作本身摧毁了「继续这份工作」的唯一凭证。现在该原因下只杀进程、`status → retired`、写一条带 `reason: "session-shutdown"` 的 `node_retired`(事件闭集仍 **16**,`reason` 是字段);`phase` / `phase_since` / `rework_count` 一律不动、不写 `phase_change`(暂停不是新 stint,区别于 #357 的 `revived`)。`isTerminalNode` 仍为 false ⇒ 可被 resume;`hasEnded` 仍为 true ⇒ 暂停期间不投邮件(§8.3.1 现状不变)。**其它 cause(`stop`/`abort`/超预算/显式 retire)逐字不变**。② **`RunRecord.sessionFile`(run.json 附加字段,非 SQLite 列)**:记录创建该 run 的会话 transcript 绝对路径,是仓内**唯一**的会话身份判据(`path.resolve` 相等;仓内没有 session id 概念,`ChildRecord.sessionId` 是死字段)。同一 session 再启动时按它找回**最近一条** run 并**复用**(同一 `runId`/run 目录/`legion.db`;root 行 update 而非 insert,`insertNode` 会按名字冲突抛 `NodeNameConflictError`;只更新 run.json 的 `updatedAt`)。找不到 / 账本损坏 / root 行缺失 ⇒ 退回「今天的行为」(新建 run),**不抛**。③ **`ChildRecord.phases`(run.json 附加字段,非 SQLite 列)**:launch 时把 root 声明的相位机器快照进 child 记录,`subagent({action:"resume"/"continue"})` 与 centurion 自愈恢复把它传回 `Orchestrator.launch`,经既有 `LEGION_PHASES_ENV` 通道下发;否则复活的内联声明队会退回角色卡机器,自定义相位名变成 `invalid_phase`。只对真正下发的角色(`centurion`)快照——把角色卡机器当**声明**回放会被 launch 期守卫(只 root 声明 / 只给 centurion)拒绝。④ **提示与边界**:root 启动时**只**在 system prompt 里列出「`status ∈ {starting,running,blocked,retired}` 且 `phase` 非空且非终态、且本进程没有在跑它」的直接子节点(附一句硬规则:不要自己 resume,先问用户),用户侧一行 `notify`,**零 spawn**(不自动恢复,不烧 token);**跨 session 不扫描旧 run、不提示、不写任何文件**。该段从账本现算,节点全部恢复后自动消失。**不改**:`NodeStatus` 仍 6 个值(不加 `interrupted`)、事件闭集仍 16、`hasEnded`/`isTerminalNode` 语义、worktree/MR/acceptance、SQLite schema(`nodes` 表不加列)。触发:issue #374。 |
|
|
591
|
+
| 2026-11-03 | REV-29 | **交付物是分支,MR 只是有 remote 时的便利**(措辞修订,非语义/非 schema/非权限变更,按 §15-1 追加):§6.2/§7.1/§9 里「squash MR」「MR 树」的表述,在**零 remote** 的仓库里无法执行(实测一次真实会话:centurion 只能交一条待推分支,报 `no remote`),而契约读起来像「没有 MR 就是没交付」。明确:**D6 的「子树 squash MR」= 一个可审查的 squash 分支(分支名 + HEAD sha)**;MR/PR 是**有 remote 且 `gh`/`glab` 可用**时的可选产物,附 URL 更好,缺失不构成验收失败,**任何情况下都不许编造 URL**。逐级合并权与「叶子永不 merge」一字不变(合并仍发生在父节点,认的是分支/提交,不是平台对象)。落地文案:`agents/centurion.md`、`src/extension/child-guard.ts` 的注入附录、验证后的 `sendSelfMessage`、`subagent` 的 worktree 参数说明、`skills/pi-legion/SKILL.md`、`README.md`、`examples/subagents-parent-context.md`。 |
|
|
@@ -51,7 +51,11 @@ const VexillatioParams = Type.Object({
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
**只有 launch 语义**(无 status/collect/wait/steer/retire——用 `subagent` 工具的既有控制动作管理同名子节点;见 2.6
|
|
54
|
+
**只有 launch 语义**(无 status/collect/wait/steer/retire——用 `subagent` 工具的既有控制动作管理同名子节点;见 2.6)。
|
|
55
|
+
|
|
56
|
+
**先于下列步骤的前置校验**:`tasks[]` 条目带 `name` → 硬拒(`INVALID_PARAMS`),消息为 ``tasks[] step <i> cannot include `name`; vexillatio allocates one handle per step itself.``。命名权归 vexillatio(§2.6 `<role>-N` 接续父节点既有 children),接受该字段等于把「调用方指定的 handle 静默失效」搬进工具人通道。
|
|
57
|
+
|
|
58
|
+
执行流程(同步):
|
|
55
59
|
|
|
56
60
|
1. 解析 agent:默认 `scout`;目标必须在 catalog 里且 `utility === true`,否则报错 `not a utility agent: <name> (mark it with 'utility: true')`。
|
|
57
61
|
2. 走**既有 `orchestrator.launch`**(复用 budget/session 管线),但 `SpawnInput.env` 显式剔除 legion 身份四变量:`PI_LEGION_DB`、`PI_LEGION_NODE_ID`、`PI_SUBAGENT_PARENT_PATH`、`PI_SUBAGENT_MAX_DEPTH`(防幽灵节点 + 防工具人继承指挥链身份)。**不剔除** `PI_SUBAGENT_CHILD`、`PI_SUBAGENT_CHILD_ROLE`、`PI_SUBAGENT_MAX_TOOL_CALLS` 等预算变量(child-guard 仍要工作)。utility 目标(两通道)**剥除 `PI_SUBAGENT_ALLOW_NESTED`**(§2.5.1,utility 语义优先);`lineageEnv` 统一设置 `PI_SUBAGENT_UTILITY_CHILD`(§2.2)。
|