pi-legion 3.0.3 → 4.0.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 +263 -2
- package/CONTRIBUTING.md +4 -17
- package/README.md +29 -22
- package/agents/worker.md +1 -1
- package/docs/contracts/display-liveness-v1.md +373 -0
- package/docs/contracts/drop-non-pi-kinds-v4.md +198 -0
- package/docs/contracts/esc-abort-v1.md +4 -4
- package/docs/contracts/esc-no-retire-v3.md +2 -2
- package/docs/contracts/launch-admission-queue-v1.md +839 -0
- package/docs/contracts/status-panel-v1.md +174 -0
- package/docs/inprocess-supervisor.md +37 -4
- package/docs/legion-v2-contract.md +18 -2
- package/docs/performance-2026-10-19.md +199 -0
- package/docs/spawn-mode-visibility-v1.md +206 -0
- package/docs/vexillatio-contract.md +3 -1
- package/index.ts +633 -84
- package/package.json +4 -2
- package/skills/pi-legion/SKILL.md +2 -3
- package/src/agents/agents.ts +17 -10
- package/src/agents/overrides.ts +0 -1
- package/src/agents/presets.ts +14 -40
- package/src/agents/settings.ts +26 -77
- package/src/agents/step-model.ts +7 -22
- package/src/api.ts +1 -35
- package/src/extension/event-bus.ts +80 -8
- package/src/extension/legion-commands.ts +43 -17
- package/src/extension/legion-doctor.ts +1 -1
- package/src/extension/legion-stop.ts +39 -13
- package/src/extension/legion-tools.ts +21 -4
- package/src/extension/liveness.ts +200 -0
- package/src/extension/notify.ts +6 -6
- package/src/extension/playbook.ts +6 -6
- package/src/extension/recycle.ts +6 -2
- package/src/extension/runtime.ts +422 -18
- package/src/extension/slash.ts +0 -21
- package/src/extension/spawn-render.ts +183 -0
- package/src/extension/summary.ts +1 -1
- package/src/legion/db.ts +165 -1
- package/src/legion/events.ts +81 -16
- package/src/legion/nodes.ts +13 -2
- package/src/legion/rebuild.ts +7 -1
- package/src/legion/usage.ts +58 -4
- package/src/runs/orchestrator.ts +1165 -62
- package/src/runs/worktree.ts +190 -21
- package/src/shared/ignored-kind.ts +57 -0
- package/src/shared/progress.ts +11 -0
- package/src/shared/session.ts +199 -19
- package/src/shared/types.ts +50 -66
- package/src/supervisor/admission-queue.ts +516 -0
- package/src/supervisor/inprocess-supervisor.ts +108 -33
- package/src/supervisor/proc-memory.ts +67 -0
- package/src/supervisor/routing-supervisor.ts +58 -1
- package/src/tui/live-overlay.ts +14 -0
- package/src/tui/session-messages-file.ts +100 -0
- package/src/tui/status.ts +103 -21
- package/src/tui/tree-view.ts +36 -3
- package/src/tui/viz/layout.ts +54 -3
- package/src/tui/viz/model.ts +67 -14
- package/src/tui/viz/palette.ts +11 -0
- package/src/tui/viz/panels.ts +222 -8
- package/src/tui/viz/railway.ts +28 -25
- package/src/tui/viz/view.ts +110 -23
- package/src/backends/legacy/client.ts +0 -638
- package/src/backends/legacy/cursor-chat.ts +0 -348
- package/src/backends/legacy/runner.ts +0 -101
- package/src/backends/legacy/types.ts +0 -138
- package/src/runs/kind.ts +0 -260
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# 展示层活性判定统一契约 v1(FROZEN)
|
|
2
|
+
|
|
3
|
+
> 状态:**FROZEN**(2026-10-24 用户裁决"直接第二步")。
|
|
4
|
+
> 冲突时**停下报告**,不得重新解释本契约。
|
|
5
|
+
> 关联:#209-3(owner 判定)、#238(burst 合并 / KNOWN LIMITATION)、#266(queued 状态)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## §1 问题陈述
|
|
10
|
+
|
|
11
|
+
用户报告(原话):
|
|
12
|
+
|
|
13
|
+
> 轨道图上有还在处理的,就是还在处理,他没了就没了,不要说什么延迟什么的,
|
|
14
|
+
> 我看起来就是时间没有对齐啊。
|
|
15
|
+
|
|
16
|
+
现象(均已复现):
|
|
17
|
+
|
|
18
|
+
1. `subagent({action:"status"})` 显示 `Status · 1 live child`,同一个 child 又显示
|
|
19
|
+
`owner: dead (pid gone)` + `state=failed` —— **死掉的 child 被计入 live**。
|
|
20
|
+
2. 后台通知说某 child `still running`,同时 `status all` 返回 `No live children`。
|
|
21
|
+
3. `collect` 还能读出该 child 的完整报告(从 JSONL 历史重放)。
|
|
22
|
+
4. 轨道图上某节点停在 `running`,而该子进程早已死亡。
|
|
23
|
+
|
|
24
|
+
用户不接受"延迟投递"作为解释。**判定:用户的判断正确——这是时间语义错误,不是延迟。**
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## §2 根因(已确证,带证据)
|
|
29
|
+
|
|
30
|
+
### R1 — "活着"没有单一权威定义,散落在 3 类地方
|
|
31
|
+
|
|
32
|
+
| 位置 | 判定方式 | 证据 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| 列表筛选 | 只看 `child.state ∉ {retired, exited}` | `index.ts:3230` |
|
|
35
|
+
| 文本渲染 | **额外**算 owner dead | `src/extension/runtime.ts:399-404` |
|
|
36
|
+
| 轨道图 | `eventOverrides` + 账本行按时间戳竞选 | `src/extension/runtime.ts:702, 837-848` |
|
|
37
|
+
|
|
38
|
+
后果:同一个 child 可以同时是 `live`(列表)和 `failed`(文本)和 `running`(轨道图)。
|
|
39
|
+
|
|
40
|
+
### R2 — 跨写入方时钟被当作单调可比
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/extension/runtime.ts:837
|
|
44
|
+
if (node && event.ts >= node.updatedAt) { // 第 1 道
|
|
45
|
+
// src/extension/runtime.ts:839
|
|
46
|
+
if (!previous || event.ts >= previous.ts) { // 第 2 道
|
|
47
|
+
// src/extension/runtime.ts:702
|
|
48
|
+
if (override && override.ts >= node.updatedAt) { // 第 3 道
|
|
49
|
+
...
|
|
50
|
+
} else {
|
|
51
|
+
if (override) eventOverrides.delete(node.id); // ← 永久删除 override
|
|
52
|
+
tree.set(node.id, { ...node }); // ← 采信账本行
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `event.ts` 由**父进程**事件总线打时间戳。
|
|
57
|
+
- `node.updatedAt` 由**子进程**写账本时打时间戳(`src/legion/nodes.ts:184`),
|
|
58
|
+
且**只在心跳时推进**:`src/extension/legion-tools.ts:145`
|
|
59
|
+
```ts
|
|
60
|
+
const alive = children.filter((child) => this.supervisor.isAlive(child.name));
|
|
61
|
+
if (alive.length === 0) return; // ← 死了就不写心跳
|
|
62
|
+
```ts
|
|
63
|
+
**子进程一死,`updatedAt` 永久冻结。**
|
|
64
|
+
|
|
65
|
+
**判定**:两个不同写入方的时钟**不构成全序**,用 `>=` 比较它们本身就不成立。
|
|
66
|
+
第 3 道在 override 比账本行旧时**把 override 永久删除**,于是:
|
|
67
|
+
|
|
68
|
+
> 一个已 `settled` 的节点,若账本行停在 `starting`(子进程死了),
|
|
69
|
+
> 下一轮 `snapshotNodes()` 会删掉 settled override 并采信 `starting`
|
|
70
|
+
> → **轨道图把已结束的节点拉回"还在跑"。**
|
|
71
|
+
|
|
72
|
+
这正是用户看到的"没有对齐"。
|
|
73
|
+
|
|
74
|
+
### R3 — 七个状态集合互不相同,且都不含 `queued`
|
|
75
|
+
|
|
76
|
+
| 集合 | 定义位置 | 内容 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `RUNNING` | `src/tui/viz/model.ts:59` | starting, working, running, blocked |
|
|
79
|
+
| `RUNNING_STATES` | `src/tui/status.ts:361` | starting, working, running |
|
|
80
|
+
| `ACTIVE_STATES` | `src/tui/status.ts:362` | RUNNING_STATES + blocked |
|
|
81
|
+
| `TERMINAL_STATES` | `src/tui/status.ts:363` | settled, success, completed, failed, aborted, retired, awaiting, unknown |
|
|
82
|
+
| `ACTIVE_STATUSES` | `src/tui/tree-view.ts:20` | starting, running, blocked |
|
|
83
|
+
| `TERMINAL_STATUSES` | `src/tui/tree-view.ts:21` | settled, failed, retired |
|
|
84
|
+
| `LIVE_NODE_STATUSES` | `src/extension/legion-status.ts:43` | starting, running, blocked |
|
|
85
|
+
| `LIVE_CHILD_STATUSES` | `src/legion/nodes.ts:25` | starting, running, blocked |
|
|
86
|
+
|
|
87
|
+
`displayStatus()`(`src/tui/viz/model.ts:72-78`)的兜底是 `return "retired"`,所以:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
displayStatus("idle") === "retired" ← idle 在轨道图上显示成"已退休"
|
|
91
|
+
displayStatus("queued") === "retired" ← queued 显示成"已退休"
|
|
92
|
+
displayStatus(undefined) === "retired"
|
|
93
|
+
```ts
|
|
94
|
+
|
|
95
|
+
### R4 — 孤儿检测只报告,不收敛
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
// src/extension/legion-tools.ts:251
|
|
99
|
+
const event = appendEvent(this.db, node.id, "orphan_detected", {...});
|
|
100
|
+
this.eventBus?.emit(event);
|
|
101
|
+
this.sendRootMessage(`Legion orphan detected at ...`);
|
|
102
|
+
// ← 没有 updateNode():node.status 仍是 running
|
|
103
|
+
```ts
|
|
104
|
+
|
|
105
|
+
`orphanStaleMs` 默认 90s(`src/shared/types.ts:571`),但**只发消息不改状态**。
|
|
106
|
+
启动期 `hygiene` 有 `node_abandoned` 路径(`src/runs/hygiene.ts`),但阈值是
|
|
107
|
+
`staleNodeHours` 默认 **72 小时**,且只在启动时跑。
|
|
108
|
+
|
|
109
|
+
### R5 — 已有代码承认过这个病,但选择了"让用户自己判断"
|
|
110
|
+
|
|
111
|
+
`src/extension/legion-tools.ts:258-264` 的注释:
|
|
112
|
+
|
|
113
|
+
> The notice can be delivered long after detection: it is queued through
|
|
114
|
+
> `subagent-notify` and the outbox flushes at `MAX_PENDING_AGE_MS` or at
|
|
115
|
+
> `agent_settled`. A stale `staleMs` alone therefore reads as "detected just
|
|
116
|
+
> now" when the value can be tens of minutes old, which makes a replayed
|
|
117
|
+
> notice look like a fresh detection. **Stamp the detection time so a late
|
|
118
|
+
> delivery is self-evident.**
|
|
119
|
+
|
|
120
|
+
**判定**:这是把时序缺陷转嫁给读者。v1 改为从根上消除,不再靠时间戳自证。
|
|
121
|
+
|
|
122
|
+
### R6 — `#238` 的 KNOWN LIMITATION 与本契约的关系
|
|
123
|
+
|
|
124
|
+
`test/unit/runtime.test.ts:3349` 明确钉住:
|
|
125
|
+
|
|
126
|
+
> a mid-burst ledger write can make the painted status differ from a per-event re-read
|
|
127
|
+
> ...
|
|
128
|
+
> reproducing a SEQUENCE of reads is not possible with a constant number of reads,
|
|
129
|
+
> which is why this is bounded and documented rather than fixed.
|
|
130
|
+
|
|
131
|
+
**判定:该结论对本契约不构成障碍,因为本契约不试图"复现读取序列"。**
|
|
132
|
+
它改变问题:**渲染不再是从事件序列累积出的状态,而是对"渲染时刻的事实"做一次纯投影。**
|
|
133
|
+
同一时刻的事实只对应一个答案,故不存在序列依赖。
|
|
134
|
+
该用例将改为断言"同一 facts 的任意读取次数得出一致结果"(见 A6)。
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## §3 决策
|
|
139
|
+
|
|
140
|
+
### D1 — 单一权威:`liveness()` 是唯一的"活着"判定
|
|
141
|
+
|
|
142
|
+
新增纯函数模块 `src/extension/liveness.ts`(**无 fs / 无 db / 无 timer / 无 clock 读取**,
|
|
143
|
+
所有输入由调用方传入):
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
export type Liveness = "live" | "dead" | "unknown";
|
|
147
|
+
|
|
148
|
+
export interface LivenessFacts {
|
|
149
|
+
/** 进程内注册表里是否存在且状态未终态。 */
|
|
150
|
+
readonly inMemory?: { readonly state: string } | undefined;
|
|
151
|
+
/** 账本行的状态与心跳时刻(可能来自已死进程)。 */
|
|
152
|
+
readonly ledger?: { readonly status: NodeStatus; readonly updatedAt: number } | undefined;
|
|
153
|
+
/** run.json 的 child 记录状态(调度层)。 */
|
|
154
|
+
readonly record?: { readonly state: string } | undefined;
|
|
155
|
+
/** pid marker 的存活判定结果;undefined = 没有 marker。 */
|
|
156
|
+
readonly pidAlive?: boolean | undefined;
|
|
157
|
+
readonly now: number;
|
|
158
|
+
readonly staleMs: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function liveness(facts: LivenessFacts): Liveness;
|
|
162
|
+
```ts
|
|
163
|
+
|
|
164
|
+
**判定顺序(全序,不是"任一为真")**:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
1. inMemory 存在且非终态 → live (最强:本进程正在持有它)
|
|
168
|
+
2. inMemory 存在且是终态 → dead
|
|
169
|
+
3. pidAlive === true → live (有进程真在跑)
|
|
170
|
+
4. pidAlive === false → dead (进程确认已亡,D4 接受窗口)
|
|
171
|
+
5. ledger 非终态 且 now - updatedAt < staleMs → live (心跳未过期)
|
|
172
|
+
6. ledger 非终态 且心跳已过期 → dead (孤儿:D7 收敛)
|
|
173
|
+
7. ledger 是终态 → dead (账本已落终态:已结束)
|
|
174
|
+
8. record 是终态 → dead (调度层已判定结束)
|
|
175
|
+
9. 其余(含全部 undefined) → unknown (绝不猜)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**为什么是顺序而非并集**:并集会让"某处残留一条 running 账本行"永久压过
|
|
179
|
+
"本进程已确认它死了"。顺序保证**更强的证据胜出**,且 `unknown` 是一个明确的
|
|
180
|
+
第三态而非"当活处理"。
|
|
181
|
+
|
|
182
|
+
### D1-a — 终态集合与未知标签(2026-10-24 父会话裁决)
|
|
183
|
+
|
|
184
|
+
W1 实施时发现契约原文漏了两处,此处补全并冻结。
|
|
185
|
+
|
|
186
|
+
**`TERMINAL_STATES`(账本/记录层终态)**
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
settled, success, completed, done,
|
|
190
|
+
failed, aborted, error, truncated,
|
|
191
|
+
retired
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**`awaiting` 不是终态。** 它表示"活着但在等下游"
|
|
195
|
+
(`src/runs/orchestrator.ts:2271`:`if (execution.status !== "running" && !isRetired()) child.state = "awaiting"`;
|
|
196
|
+
`src/extension/recycle.ts:23` 也把它单列)。把它当终态,会让一个仍在等待的 centurion
|
|
197
|
+
在展示层凭空消失。`awaiting` 归入**非终态**,展示为灰(同 `idle`,见 D5)。
|
|
198
|
+
|
|
199
|
+
> `src/tui/status.ts:363-372` 现有的 `TERMINAL_STATES` 把 `awaiting` 当终态,且
|
|
200
|
+
> `status.ts:525-528` 先把它改写成 `"waiting"` 再渲染。**W4 必须一并纠正**:
|
|
201
|
+
> `awaiting` 既不消失,也不显示为终态。
|
|
202
|
+
|
|
203
|
+
**未知标签:用补集,不用白名单。** 判定"非终态"用 `!isTerminalState(status)`,
|
|
204
|
+
**不是** `NON_TERMINAL.has(status)`。认不出的标签不应让我们宣布其生命周期结束;
|
|
205
|
+
白名单式判定会让 W2 传入真实账本行时静默掉进 `unknown`。
|
|
206
|
+
`NON_TERMINAL` 仅作文档性导出,不参与判定。
|
|
207
|
+
|
|
208
|
+
**`LivenessFacts.ledger.status` 用 `string`,不用 `NodeStatus`。**
|
|
209
|
+
`idle`/`queued` 按 D9 永不进入 `NODE_STATUSES` 闭集,但 D5 要求它们是一等展示态;
|
|
210
|
+
冻结成 `NodeStatus` 会迫使 W2/W4 处处 `as` 强转,反而模糊 D9 的边界。
|
|
211
|
+
|
|
212
|
+
### D2 — 三态而非二态
|
|
213
|
+
|
|
214
|
+
`unknown` 必须存在且**不得**被当作 `live` 展示。
|
|
215
|
+
理由:把未知当活是当前 `status` 显示 `1 live child`(owner 已死)的直接原因;
|
|
216
|
+
把未知当死会误杀正在启动的 child。第三态是唯一诚实的选项。
|
|
217
|
+
|
|
218
|
+
### D3 — 跨写入方时钟不可比较(硬禁令)
|
|
219
|
+
|
|
220
|
+
**禁止**在任何地方用 `>=`/`<=`/`>`/`<` 比较**来自不同写入方**的时间戳。
|
|
221
|
+
|
|
222
|
+
具体:`event.ts`(父进程总线)与 `node.updatedAt`(子进程账本)之间的比较
|
|
223
|
+
**全部删除**。事件只用于**触发重算**,不再用于**决定谁的结论胜出**。
|
|
224
|
+
|
|
225
|
+
验收:`src/` 下不存在 `event.ts` 与 `updatedAt` 的直接比较。
|
|
226
|
+
|
|
227
|
+
### D4 — pid 窗口接受(用户裁决 1)
|
|
228
|
+
|
|
229
|
+
`pidAlive === false` 立即判 `dead`。不做心跳宽限、不加第二套时间语义。
|
|
230
|
+
理由:pid 探测是最接近操作系统事实的判据;加宽限等于重新引入"时间没对齐"。
|
|
231
|
+
|
|
232
|
+
已知窗口:进程刚退出、`isPidAlive` 尚未观察到的**秒级**窗口,接受。
|
|
233
|
+
|
|
234
|
+
### D5 — `idle` 与 `queued` 是一等展示态(用户裁决 2)
|
|
235
|
+
|
|
236
|
+
`displayStatus()` 的兜底 `retired` 必须拆开:
|
|
237
|
+
|
|
238
|
+
| 输入 | 输出(`VizStatus`) | 展示 |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `idle` | `idle`(新) | **灰**:`idle · 占槽待唤起` |
|
|
241
|
+
| `queued` | `queued`(新) | `queued(n)` |
|
|
242
|
+
| `starting` / `working` / `running` / `blocked` | `running` | 不变 |
|
|
243
|
+
| `settled` / `success` / `completed` / `done` | `settled` | 不变 |
|
|
244
|
+
| `failed` / `aborted` / `error` / `truncated` | `failed` | 不变 |
|
|
245
|
+
| `retired` | `retired` | 不变 |
|
|
246
|
+
| 其余 / undefined | `unknown`(新) | `unknown`,**不显示为 retired** |
|
|
247
|
+
|
|
248
|
+
**为什么 `idle` 要显示而不是隐藏**:`idle` 的 child 仍然活着、仍占并发槽(F9),
|
|
249
|
+
两个事实都要传达。标灰是"活着但没在干活"的准确表达。
|
|
250
|
+
|
|
251
|
+
### D5-a — `LogEvent.kind` 不随 `VizStatus` 放宽(2026-10-24 父会话裁决)
|
|
252
|
+
|
|
253
|
+
W4 实施时暴露一处契约未覆盖的连带影响,此处冻结。
|
|
254
|
+
|
|
255
|
+
`src/tui/viz/panels.ts` 的 `LogEvent.kind` 是
|
|
256
|
+
`"spawn" | "settled" | "failed" | "retired"`,`buildLog()` 原先把 `node.status`
|
|
257
|
+
直接塞进去(`node.status !== "running"` 即视为结束)。`VizStatus` 放宽后这行必然报错。
|
|
258
|
+
|
|
259
|
+
**裁决:`LogEvent.kind` 保持不变,`buildLog` 只对真正的结束态产生结束行。**
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
结束态 = settled | failed | retired
|
|
263
|
+
非结束态 = running | idle | queued | unknown → 只有 spawn 行,没有结束行
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
理由:`idle`/`queued` 按 D5 **是活的**(占着槽,只是没在干活),`unknown` **未定**。
|
|
267
|
+
把任何一项记成结束都会重新制造"活着的节点在 git-log 里已结束"的错误——
|
|
268
|
+
与本契约要消灭的问题同类。`panels.ts` 归 **W4** 文件所有权。
|
|
269
|
+
|
|
270
|
+
### D6 — 所有出口从 `liveness()` 派生
|
|
271
|
+
|
|
272
|
+
以下出口**必须**调用 `liveness()`,不得自行判定:
|
|
273
|
+
|
|
274
|
+
1. `subagent({action:"status"})` 列表筛选与头部计数(`index.ts:3223-3272`)
|
|
275
|
+
2. `renderLiveChildStatus` 的 `state`/`owner` 行(`src/extension/runtime.ts:399-416`)
|
|
276
|
+
3. `/legion` galaxy 全视图(`src/tui/viz/model.ts`)
|
|
277
|
+
4. `/legion --tree` 与 paged tree(`src/tui/tree-view.ts`)
|
|
278
|
+
5. `/legion status`(`src/extension/legion-status.ts`)
|
|
279
|
+
6. 状态小部件 / strip(`src/tui/status.ts` 的 `filterLiveTree` / `isActiveEntry`)
|
|
280
|
+
7. completion notice 投递前的终态复核(`src/extension/runtime.ts` 的 `flushOutbox`)
|
|
281
|
+
|
|
282
|
+
**不再**允许出现局部的 `state !== "retired" && state !== "exited"` 式手写判定。
|
|
283
|
+
|
|
284
|
+
### D7 — 孤儿必须收敛,不只是报告
|
|
285
|
+
|
|
286
|
+
`scanOrphans` 命中(`legion-tools.ts:241-252`)时,除现有事件与消息外,
|
|
287
|
+
**必须**把账本行收敛到终态:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
updateNode(db, node.id, { status: "failed" }, timestamp);
|
|
291
|
+
appendEvent(db, node.id, "node_abandoned", { reason: "no_active_supervisor", ... });
|
|
292
|
+
|
|
293
|
+
理由:`orphan_detected` 只描述"发现了孤儿",不改变事实;而用户要的是
|
|
294
|
+
"他没了就没了"。收敛后 `liveness()` 的第 5 步自然给出 `dead`,无需任何展示层特判。
|
|
295
|
+
|
|
296
|
+
**不得**复用 `hygiene` 的 72 小时阈值;孤儿判定阈值是 `orphanStaleMs`(默认 90s)。
|
|
297
|
+
|
|
298
|
+
### D8 — 渲染是纯投影,不是事件重放
|
|
299
|
+
|
|
300
|
+
`snapshotNodes()` / `refreshUi()` / `vizSnapshot()` 的语义改为:
|
|
301
|
+
|
|
302
|
+
> 在时刻 T 读取一次事实(进程内注册表 + 账本 + run.json + pid),
|
|
303
|
+
> 用 `liveness()` 投影出展示状态。
|
|
304
|
+
|
|
305
|
+
`eventOverrides` 降级为**纯延迟优化**(事件到达时先乐观更新,减少一次读库),
|
|
306
|
+
但**每次 `refreshUi()` 必须用事实校正**。校正规则:无条件以 `liveness()` 结果为准,
|
|
307
|
+
**不做时间戳竞选**。
|
|
308
|
+
|
|
309
|
+
### D9 — 保留 `NODE_STATUSES` 闭集
|
|
310
|
+
|
|
311
|
+
`queued` / `idle` **仍然不得**进入 `src/legion/nodes.ts` 的 `NODE_STATUSES`
|
|
312
|
+
(六值闭集,两条 SQL 硬编码六个 `?` 占位符)。它们是**展示态**,不是账本态。
|
|
313
|
+
`test/unit/legion-nodes.test.ts` 的 A16 tripwire 保持不变。
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## §4 实施切分
|
|
318
|
+
|
|
319
|
+
| 单元 | 文件 | 内容 |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| **W1** | `src/extension/liveness.ts`(新) | D1 `liveness()` + D2 三态 + D5 状态映射表;纯函数,无 I/O |
|
|
322
|
+
| **W2** | `src/extension/runtime.ts` | D3 删除三处时钟比较;D6 出口 2/5/7;D8 纯投影 |
|
|
323
|
+
| **W3** | `index.ts` | D6 出口 1(`listLiveChildrenForStatus` + `renderStatusOverview`) |
|
|
324
|
+
| **W4** | `src/tui/viz/model.ts`, `src/tui/viz/palette.ts`, `src/tui/viz/panels.ts`, `src/tui/status.ts`, `src/tui/tree-view.ts` | D5 新展示态 + D6 出口 3/4/5/6,集合归一 |
|
|
325
|
+
| **W5** | `src/extension/legion-tools.ts` | D7 孤儿收敛 |
|
|
326
|
+
| **W6** | `docs/`, `CHANGELOG.md` | 文档与迁移说明 |
|
|
327
|
+
|
|
328
|
+
**文件归属(禁止越界)**:每个 W 只能改自己那一行的文件。
|
|
329
|
+
`src/extension/liveness.ts` 由 W1 独占创建,其余 W 只读引用。
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## §5 验收
|
|
334
|
+
|
|
335
|
+
| 编号 | 断言 | 归属 |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| **A1** | `liveness()` 的 9 条判定顺序逐条覆盖;`unknown` 不被展示为 live | W1 |
|
|
338
|
+
| **A2** | 已 `settled` 的节点,账本行冻在 `starting` 且心跳过期 → 渲染为终态,**不被拉回 running**(R2 回归) | W1+W2 |
|
|
339
|
+
| **A3** | `owner: dead` 的 child **不计入** `Status · N live`(改正 `status-action.test.ts:301`) | W3 |
|
|
340
|
+
| **A4** | `displayStatus("idle") === "idle"`、`("queued") === "queued"`、`("awaiting") === "awaiting"`、`(undefined) === "unknown"` | W4 |
|
|
341
|
+
| **A5** | 孤儿超过 `orphanStaleMs` → 账本行变 `failed` + `node_abandoned` 事件;`liveness()` 随后给 `dead` | W5 |
|
|
342
|
+
| **A6** | 同一组 facts 连续投影 100 次结果恒定(替代 `#238` KNOWN LIMITATION 的收敛断言) | W1+W2 |
|
|
343
|
+
| **A7** | `src/` 中不存在 `event.ts` 与 `updatedAt` 的比较(D3 静态守卫) | W2 |
|
|
344
|
+
| **A8** | `queued`/`idle` 未进入 `NODE_STATUSES`;A16 tripwire 仍绿 | W4 |
|
|
345
|
+
| **A9** | `buildLog` 对 `idle`/`queued`/`unknown` 节点不产生结束行(D5-a) | W4 |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## §6 迁移影响(已知会打翻的既有断言)
|
|
350
|
+
|
|
351
|
+
| 测试 | 现有断言 | 处置 |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `test/unit/status-action.test.ts:301` | `/Status · 1 live child/`(死 child 算活) | **改为** `0 live` + 单独列出 dead |
|
|
354
|
+
| `test/unit/status-action.test.ts:302-304` | `state=failed` + `owner: dead` | 保留文案,但不再计入 live |
|
|
355
|
+
| `test/unit/runtime.test.ts:3349` | `#238` KNOWN LIMITATION「settled vs retired 分叉」 | **改为** A6 的收敛断言 |
|
|
356
|
+
| `test/unit/legion-viz.test.ts:64-65` | `displayStatus("idle") === "retired"` | **改为** `=== "idle"` |
|
|
357
|
+
| `test/unit/legion-status.test.ts`(12 条) | 逐行文本锁 | 逐条复核,`state=` 词表可能变 |
|
|
358
|
+
| `test/unit/status.test.ts`(36 条) | 小部件清空/折叠 | 复核 `idle` 从"消失"变"标灰" |
|
|
359
|
+
|
|
360
|
+
**要求**:每一处改动必须在 commit message 里写明"为什么旧断言是错的",
|
|
361
|
+
不得只改数字。
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## §7 禁令
|
|
366
|
+
|
|
367
|
+
1. **禁止**用时间戳比较决定展示状态(D3)。
|
|
368
|
+
2. **禁止**把 `unknown` 当 `live` 或当 `dead` 处理(D2)。
|
|
369
|
+
3. **禁止**给 pid 判定加心跳宽限或第二套时间语义(D4)。
|
|
370
|
+
4. **禁止**把 `idle`/`queued` 塞进 `NODE_STATUSES` 闭集(D9)。
|
|
371
|
+
5. **禁止**在展示层手写 `state !== "retired" && state !== "exited"` 式局部判定(D6)。
|
|
372
|
+
6. **禁止**让 `orphan_detected` 只发消息不改状态(D7)。
|
|
373
|
+
7. **禁止**在 `liveness.ts` 里读 `Date.now()` / fs / db / timer(D1 纯函数约束)。
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# 冻结契约:删除 cursor / 第三方后端支持,只保留 `kind: pi`(v4.0.0)
|
|
2
|
+
|
|
3
|
+
状态:**FROZEN** — 实施中禁止偏离。发现矛盾立刻停下,报给父会话,**不要自行解释矛盾**。
|
|
4
|
+
|
|
5
|
+
## 目标
|
|
6
|
+
|
|
7
|
+
`pi-legion` 只支持 `kind: pi`。删除 (a) cursor 专属 kind 逻辑,(b) 归档 herdr 后端
|
|
8
|
+
`src/backends/legacy/`,(c) 死掉的 `herdr.*` 设置与常量,(d) 一个派生缺陷(#233:未知事件
|
|
9
|
+
类型冻结事件游标)。
|
|
10
|
+
|
|
11
|
+
**不删除**:`spawnMode` 的 `rpc` / `inprocess` 两个 supervisory backend(那是 pi 的两种运行
|
|
12
|
+
方式,与第三方后端无关)。任何声称「backend 也该删」的推论都是错的。
|
|
13
|
+
|
|
14
|
+
## 已实测的硬事实(禁止重新侦察)
|
|
15
|
+
|
|
16
|
+
本 worktree `npm ci` 已完成;基线在 HEAD `35edac5` 上实测:
|
|
17
|
+
|
|
18
|
+
| 命令 | 结果 |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `npm run typecheck` | exit 0 |
|
|
21
|
+
| `npm test` | 1153 pass / 0 fail |
|
|
22
|
+
| `npm run test:perf` | 17 pass / 0 fail |
|
|
23
|
+
| `npm run test:integration` | 83 pass / 0 fail |
|
|
24
|
+
| `npm run test:mock-llm` | 24 pass / 0 fail |
|
|
25
|
+
|
|
26
|
+
删除规模(静态统计,非抽样):
|
|
27
|
+
|
|
28
|
+
| 触点 | 事实 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `src/backends/legacy/**` import 点 | 生产代码仅 `src/api.ts:21-30,341`;`index.ts` 零引用 |
|
|
31
|
+
| `test/legacy/**` import 点 | `test/legacy/herdr-client.test.ts:12-15`、`test/legacy/cursor-chat.test.ts:13`、`test/legacy/helpers/fake-herdr.ts:16`、`test/unit/output-cap.test.ts:6` |
|
|
32
|
+
| `test/unit/output-cap.test.ts` | **78 行 6 个 test,只有第 6 个(`legacy runner kills an infinite stream at the cap`)用 legacy runner**,其余 5 个测 `src/shared/output-cap.ts`。**禁止整文件删。** |
|
|
33
|
+
| `AGENT_KINDS` 收敛后的破测面 | 15 个测试文件(见 D4 清单) |
|
|
34
|
+
| `assertKindModelCoherent` 链 | `src/agents/presets.ts:167-183`(定义)+ `src/agents/step-model.ts:94-102`(唯一调用点) |
|
|
35
|
+
| `nativeModelFor` 存活用途 | 仅 `src/runs/orchestrator.ts:844`,且**永久传 `"pi"`**;等价于 `applyThinkingSuffix`(`src/runs/args.ts`) |
|
|
36
|
+
| `herdrStatus` 生产写入点 | **0**。`src/tui/status.ts` 内 8 处全读,`test/unit/status.test.ts:296` 是唯一写入者(测试造数) |
|
|
37
|
+
| `run.herdr` 字段 | **活跃字段**:写入点 `index.ts:421,464,1701,2294`、`src/runs/orchestrator.ts:707`、`src/runs/store.ts:256,277`,承载 `{supervisor, node_id}`。**禁止删字段** |
|
|
38
|
+
| `DEFAULTS` 7 个 herdr 键 | `startRetries`/`startRetryBackoffMs`/`maxConcurrentAgents`/`sessionRetentionDays`/`startTimeoutMs`/`binaryProbeTimeoutMs` 全仓 **0 引用**;`commandTimeoutMs` 仅在 `src/backends/legacy/client.ts` 用 |
|
|
39
|
+
| 用户可见的 `herdr` 字面量 | `src/extension/playbook.ts:19`("using herdr pane/tab/agent commands")、`src/tui/status.ts:555`(`● Async agents · herdr`) |
|
|
40
|
+
| CHANGELOG 锚点 | `CHANGELOG.md:439` 指向 `README.md#herdr-transition-and-role-migration` → 改 README 章节标题必须同步改它 |
|
|
41
|
+
|
|
42
|
+
## 决策(D 编号)
|
|
43
|
+
|
|
44
|
+
**D1 — 兼容性策略:硬删,但「被忽略的角色/预设字段」要响一次。**
|
|
45
|
+
- `AGENT_KINDS` 收敛为 `["pi"]`。**`AgentKind` 类型保留**(值域收窄),`AgentConfig.kind` 字段保留
|
|
46
|
+
(大量 plumbing 依赖它,删字段是另一场改动)。
|
|
47
|
+
- 用户角色卡里的 `kind: cursor` 等:`parseAgentDocument` 保持 `enumOr` 语义(fallback `"pi"`),
|
|
48
|
+
**但**当 frontmatter 显式写了 `kind` 且不在合法集内时,`console.error` 一次,含角色名、原值、
|
|
49
|
+
以及「v4 只支持 kind: pi,该字段被忽略(按 pi 启动)」。同样的响一次也用于
|
|
50
|
+
`subagents.presets.<name>.kind` 与 `agentOverrides.<name>.kind`(这三处是用户唯一能写 kind 的入口)。
|
|
51
|
+
- **不做**旧前缀别名(`kimi/k2` → `cb/kimi-k2` 之类)。模型名不在本契约范围。
|
|
52
|
+
|
|
53
|
+
**D2 — 删 `cursorModel` 与整条 kind 分支,`nativeModelFor` 直接消失。**
|
|
54
|
+
- `src/runs/kind.ts` 整文件删除(`planKindStart` 生产调用者为 0,见上表)。
|
|
55
|
+
`nativeModelFor` / `isPiShapedModel` / `cursorModel` / `planKindStart` 全部删除。
|
|
56
|
+
- `src/runs/orchestrator.ts:844` 改为 `applyThinkingSuffix(model, thinking)`;
|
|
57
|
+
`dropped` / `modelDropped` / `ChildRecord.modelDropped`(`src/shared/types.ts:366`)与
|
|
58
|
+
`index.ts:1993-1997` 的「runs on X's own default」提示全部删除 —— 现在不存在「pi 表达不了的模型」
|
|
59
|
+
这类情形,只有「没设置模型」。
|
|
60
|
+
- `applyThinkingSuffix` 的语义差异必须验证:`nativeModelFor("pi", m, t)` ===
|
|
61
|
+
`applyThinkingSuffix(m, t)`(`applyThinkingSuffix` 的 `replaceExisting` 默认 `false` 与 `nativeModelFor`
|
|
62
|
+
一致)。**在删 `nativeModelFor` 之前先写一条等价断言**(D8 覆盖)。
|
|
63
|
+
- `assertKindModelCoherent` 与 `src/agents/step-model.ts` 的调用点一并删除(它的唯一用途是
|
|
64
|
+
「模型的 kind 与 preset 的 kind 不一致时拒绝」,kind 只有 pi 之后不存在不一致)。`PresetConfig.kind`
|
|
65
|
+
字段保留但只接受 `"pi"`(由 D1 的响一次覆盖非 pi 值)。
|
|
66
|
+
|
|
67
|
+
**D3 — 删 `src/backends/legacy/**` 与 `test/legacy/**`。**
|
|
68
|
+
- `src/api.ts`:删 `:19-30`(deprecated herdr transport 段)、`:329-341`(legacy types 段)、
|
|
69
|
+
`:194-200` 里的 `cursorModel`/`nativeModelFor`/`planKindStart`/`KindStartPlan`/`isPiShapedModel`。
|
|
70
|
+
- `test/unit/api.test.ts:9` 的 `createHerdrClient` 断言删除(**不是**改成 false,直接删这一行),
|
|
71
|
+
其余 4 条断言保留。
|
|
72
|
+
- `test/unit/output-cap.test.ts`:只删 `:6` 的 import 与第 6 个 test(`:57-63` 附近),其余 5 个保留。
|
|
73
|
+
- `tsconfig.json:18` 的 `include` 覆盖 `test/**/*.ts`,所以 `test/legacy` 的删除必须与
|
|
74
|
+
`src/backends` 的删除**同一个 commit**,否则 typecheck 必红。
|
|
75
|
+
- `package.json:56` 的 `test:docker` echo 文案里的 legacy 措辞改为中性(只改文案,命令不变)。
|
|
76
|
+
|
|
77
|
+
**D4 — `AGENT_KINDS` 收敛会打红 15 个测试文件,必须同批修复。**
|
|
78
|
+
完整清单(`rg -c` 实测命中数):
|
|
79
|
+
`test/unit/step-model.test.ts`、`test/unit/presets.test.ts`、`test/unit/kind.test.ts`(整文件删)、
|
|
80
|
+
`test/unit/agents-listing.test.ts`、`test/unit/teams.test.ts`、`test/unit/status.test.ts`、
|
|
81
|
+
`test/unit/summary.test.ts`、`test/unit/agents.test.ts`、`test/unit/inprocess-supervisor.test.ts`、
|
|
82
|
+
`test/unit/context-injection.test.ts`(`planKindStart` 相关 2 个 test 删)、
|
|
83
|
+
`test/unit/presets-regression.test.ts`、`test/unit/notify.test.ts`、
|
|
84
|
+
`test/integration/orchestrator.test.ts`、`test/unit/agents-model.test.ts`、`test/unit/provider-pin.test.ts`。
|
|
85
|
+
- 非 pi kind 的 test 用例**直接删除**,不要改成 `assert.throws`(拒绝逻辑本身也不存在了)。
|
|
86
|
+
- `test/unit/agents-listing.test.ts:377,393` 的 `cannot be used with kind 'cursor'` /
|
|
87
|
+
`dropped at launch: kind 'cursor' runs on its own default` 断言删除(`src/extension/slash.ts:279-292`
|
|
88
|
+
的整段提示随 D2 一起删)。
|
|
89
|
+
|
|
90
|
+
**D5 — 删死掉的 `herdr.*` 设置面(保留 `run.herdr` 字段)。**
|
|
91
|
+
- 删:`src/agents/settings.ts:14`(import)、`:91-92`、`parseHerdrSettings` 全函数(`:119-185`)、
|
|
92
|
+
`:480-481` 调用、`:817` 注释里的 `herdr`、`:866-868` 合并;
|
|
93
|
+
`src/shared/types.ts` 的 `HerdrSettings`(`:519-533`)、`SubagentsSettings.herdr`(`:702-703`)、
|
|
94
|
+
`DEFAULTS` 里 7 个 herdr 键(`:790-807`,改造后 `DEFAULTS` 只剩 `turnTimeoutMs`/`settleQuietMs`)、
|
|
95
|
+
`ErrorCodes.HERDR_UNAVAILABLE`(`:769`)与 `ErrorCodes.PANE_BUSY`(`:772`)。
|
|
96
|
+
- **`RunRecord.herdr`(`types.ts:382-388`)保留原名不删**(活跃字段,改名是另一场改动)。
|
|
97
|
+
- 旧 settings 文件里残留 `subagents.herdr` 时:`parseHerdrSettings` 被删后该键**静默丢弃**
|
|
98
|
+
(与 `parseInProcessLimits` 的「未知键忽略」一致)。**这是可接受的**,因为该块自 2.0.0 起已被忽略、
|
|
99
|
+
且当前会打 deprecation 警告——警告随解析器一起消失。契约声明:**不做**新的「未知键报警」机制。
|
|
100
|
+
|
|
101
|
+
**D6 — 用户可见的 `herdr` 措辞清理。**
|
|
102
|
+
- `src/tui/status.ts:555` 的 widget 标题 `● Async agents · herdr` → `● Async agents`。
|
|
103
|
+
**同步改 `test/unit/status.test.ts:195` 的断言**(这是硬编码字面量)。
|
|
104
|
+
- `src/extension/playbook.ts:19`:「using herdr pane/tab/agent commands for dispatch」→
|
|
105
|
+
「launching child Pi processes by hand (use the subagent tool)」。该字符串会进 LLM 上下文,措辞要准。
|
|
106
|
+
- `src/extension/summary.ts:620` 的 `run.herdr.supervisor ?? "legacy"` → `?? "unknown"`。
|
|
107
|
+
- `herdrStatus` 字段(`src/tui/status.ts:36` 及 8 处读)**保留不删**:它是 `StatusEntry` 的
|
|
108
|
+
`Pick<>` 成员,且 `test/unit/status.test.ts:296` 造数依赖;改名是另一场改动。
|
|
109
|
+
|
|
110
|
+
**D7 — #233:事件类型闭集 + 轮询容错,两件事都做。**
|
|
111
|
+
- (a) `node_abandoned` 加入 `src/legion/events.ts` 的 `LEGION_EVENT_TYPES`(现 14 → 15 项)。
|
|
112
|
+
同步 `test/unit/legion-events.test.ts:20` 的 `assert.equal(LEGION_EVENT_TYPES.length, 14)`。
|
|
113
|
+
`src/runs/hygiene.ts:275` 的 `RETAINED_EVENT_TYPES` 已含它,不动。
|
|
114
|
+
- (b) `src/extension/event-bus.ts` 的 `poll()` 容错:**一条毒行不得冻结游标**。要求:
|
|
115
|
+
- `readEventsAfter` 抛出「未知类型 / 数据 JSON 损坏」这类**单行**错误时,游标**越过该行**并继续,
|
|
116
|
+
`onError` 照常被调用(每个毒行报一次,不重复刷屏)。
|
|
117
|
+
- 实现方式由 worker 定,但必须满足可验证性:★verifier 用真实 SQLite 造 3 行
|
|
118
|
+
(`node_launched` / `node_abandoned` 快照行 / `phase_change`),断言
|
|
119
|
+
`delivered === 2`、`cursor === 3`、`errors.length === 1`;
|
|
120
|
+
再 poll 一次,断言 `errors.length` 仍为 1(不重复报)、`cursor === 3`(不自旋)。
|
|
121
|
+
- **不要**改 `readEventsAfter` 的签名或让它静默吞错:`readEventsAfter` 的严格性(`test/unit/legion-events.test.ts`
|
|
122
|
+
的 `assert.throws(appendEvent(..., "unknown" as never))`)必须保留。容错只发生在 poller。
|
|
123
|
+
- **不要**在 `LegionEventBus.emit` 路径上加容错:那是进程内事件,类型由 TS 保证。
|
|
124
|
+
|
|
125
|
+
**D8 — #233 的回归测试要「翻转」,不要「删掉」。**
|
|
126
|
+
- `test/perf/event-bus.test.ts` 的最后一个 test(标题含 `REGRESSION (open defect)`):
|
|
127
|
+
标题改成不带 open-defect 的表述,断言翻转为「毒行之后的合法事件仍然送达」。
|
|
128
|
+
- 另在 `test/unit/legion-event-bus.test.ts` 增加一条**非 perf** 的容错回归(perf 套件不在 CI 里跑,
|
|
129
|
+
这条必须进 CI)。可用 `makeClock()` 现有的假时钟模式。
|
|
130
|
+
- D2 的 `nativeModelFor("pi") === applyThinkingSuffix` 等价断言,加在删除 `nativeModelFor` **之前**跑一次
|
|
131
|
+
即可(不要求留在仓库里)。
|
|
132
|
+
|
|
133
|
+
**D9 — 文档面最小改动,历史文档不重写。**
|
|
134
|
+
- **改(产品面,5 处)**:
|
|
135
|
+
1. `README.md:87-89` —— 删「`kind !== "pi"` 会被拒」的整句。顺手修掉同段的悬空引用
|
|
136
|
+
「see Isolation below」(README 无 `## Isolation` 标题)。**不要**改 `README.md:217` 的
|
|
137
|
+
`legion_mail({ ..., kind? })`(邮件 kind,同名不同物)。
|
|
138
|
+
2. `README.md:508-528` —— 章节标题改为 `## Supported kinds`(**保留** `herdr` 历史交代的正文),
|
|
139
|
+
同步 `README.md:33` 的 Contents 项与 `CHANGELOG.md:439` 的锚点。整节重写为:
|
|
140
|
+
「v4 只有 `kind: pi`;`kind` 仍是合法 frontmatter 键但只接受 `pi`;非 pi 值被忽略并按 pi 启动」。
|
|
141
|
+
3. `docs/inprocess-supervisor.md:14`(fallback 触发条件去掉 non-pi)与 `:190-192`(Refusals 里的
|
|
142
|
+
non-pi 条目删除)。
|
|
143
|
+
4. `skills/pi-legion/SKILL.md:85-87` —— 这是**运行期注入 agent prompt 的文本**,优先级最高:
|
|
144
|
+
把「Pi Legion v2 supports `kind: pi`; for a role that requires a non-Pi CLI, use the Herdr-era
|
|
145
|
+
0.16.x release line」改成「Pi Legion runs `kind: pi` roles only」之类的现行表述。
|
|
146
|
+
5. `CONTRIBUTING.md:110-113`(deprecated herdr 导出段)与 `:164-175`(Legacy Herdr / kind checks 段):
|
|
147
|
+
前者的 helper 已删,整段重写;后者指向已删的 `test/legacy/docker/verify-13-kinds.sh`,整段删除。
|
|
148
|
+
- **不改(历史设计文档)**:`docs/design.md`、`docs/legion-v2-contract.md`(除 §15 追加一条 REV)、
|
|
149
|
+
`docs/legion-v2-delivery.md`、`docs/tui-viewer-contract.md`、`docs/contracts/spawn-default-inprocess-v3.md`、
|
|
150
|
+
`docs/contracts/osc-busy-plugin.md`、`docs/performance-2026-10-19.md`。
|
|
151
|
+
在 `docs/legion-v2-contract.md` 的 REV 表(`:448-460`)**追加一行** REV-13:`v4 收紧为仅 kind: pi;
|
|
152
|
+
AGENT_KINDS 收敛为 ["pi"];非 pi 值忽略并按 pi 启动`。
|
|
153
|
+
- **假阳性白名单(禁止改动)**:`docs/tui-viewer-contract.md` 的 `cursor`/`kind:` 全是文件游标与
|
|
154
|
+
ChatItem 判别式;`docs/performance-2026-10-19.md` 的 `cursor` 全是事件游标;
|
|
155
|
+
`docs/legion-v2-contract.md:173,313,355` 是邮件 kind;`docs/design.md:416` 是 artifact kind。
|
|
156
|
+
|
|
157
|
+
**D10 — 版本与 CHANGELOG。**
|
|
158
|
+
- `package.json` version → `4.0.0`,`package-lock.json:3,9` 同步(`npm install --package-lock-only`)。
|
|
159
|
+
- `CHANGELOG.md`:`## [Unreleased]` 下写成 `## [4.0.0] — 2026-10-19`,用既有 `### Breaking` 标题
|
|
160
|
+
(唯一先例在 `CHANGELOG.md:379`),并补 `[4.0.0]:` 链接定义。
|
|
161
|
+
- **不推 tag、不发版**。发版由父会话在 review 通过后决定。
|
|
162
|
+
|
|
163
|
+
## 文件所有权(并行 writer 之间)
|
|
164
|
+
|
|
165
|
+
- **T1 writer(events,#233)**:`src/legion/events.ts`、`src/extension/event-bus.ts`、
|
|
166
|
+
`test/unit/legion-events.test.ts`、`test/unit/legion-event-bus.test.ts`、`test/perf/event-bus.test.ts`。
|
|
167
|
+
禁止触碰其它文件。
|
|
168
|
+
- **T2 writer(kind/backend 删除)**:`src/shared/types.ts`、`src/api.ts`、`src/runs/kind.ts`(删)、
|
|
169
|
+
`src/runs/orchestrator.ts`、`src/agents/{agents,presets,step-model,settings,overrides}.ts`、
|
|
170
|
+
`src/extension/{slash,summary,playbook}.ts`、`src/tui/status.ts`、`src/backends/**`(删)、
|
|
171
|
+
`test/legacy/**`(删)、`test/unit/*`(除 T1 的四个文件)、`test/integration/orchestrator.test.ts`、
|
|
172
|
+
`tsconfig.json`(不改内容,仅确认 include 仍成立)、`package.json:56` 文案。
|
|
173
|
+
禁止触碰 `src/legion/**` 与 `src/extension/event-bus.ts`。
|
|
174
|
+
- **T3 writer(文档 + 版本)**:`README.md`、`docs/inprocess-supervisor.md`、`docs/legion-v2-contract.md`
|
|
175
|
+
(仅 REV 表追加一行)、`skills/pi-legion/SKILL.md`、`CONTRIBUTING.md`、`CHANGELOG.md`、
|
|
176
|
+
`package.json`(仅 version)、`package-lock.json`。
|
|
177
|
+
禁止触碰 `src/**` 与 `test/**`。
|
|
178
|
+
- 三个 writer 的改动集**不相交**。T1 与 T2 都碰 `test/unit/`,但文件不重叠;T2 的清单里已显式排除 T1 的四个文件。
|
|
179
|
+
|
|
180
|
+
## 验收(每个 writer 的 verify,由父会话在本 worktree 执行)
|
|
181
|
+
|
|
182
|
+
T1:`npm run typecheck` && `npm test` && `npm run test:perf`
|
|
183
|
+
T2:`npm run typecheck` && `npm test` && `npm run test:integration` && `npm run test:mock-llm`
|
|
184
|
+
T3:`npm run typecheck`(文档不改代码,这道只保证 package.json 改动没打断类型检查)+ 父会话人工读改后的文档段。
|
|
185
|
+
|
|
186
|
+
**全绿判据**:`npm run typecheck` exit 0;`npm test` 0 fail;`npm run test:integration` 0 fail;
|
|
187
|
+
`npm run test:mock-llm` 0 fail;`npm run test:perf` 0 fail。用例总数会下降(删测试),
|
|
188
|
+
**只允许因删除非 pi kind 用例而下降,任何断言级失败都是 rejected**。
|
|
189
|
+
|
|
190
|
+
## 明确不做(防止越界返工)
|
|
191
|
+
|
|
192
|
+
1. **不删** `spawnMode` 的 `rpc`/`inprocess`,不删 `RoutingSupervisor`。
|
|
193
|
+
2. **不删** `AgentConfig.kind` 字段、`LegionNode.kind` 列、`RunRecord.herdr`、`StatusEntry.herdrStatus`。
|
|
194
|
+
3. **不删** `LEGACY_EXTRA_AGENT_DIRS_ENV`(`PI_HERDR_SUBAGENTS_EXTRA_AGENT_DIRS` 别名)——
|
|
195
|
+
与 kind 无关,见 README:456 的「至少保留一个 minor」承诺。
|
|
196
|
+
4. **不改** `docs/design.md` 等历史文档的既有正文。
|
|
197
|
+
5. **不改** `readEventsAfter` / `appendEvent` 的签名与严格性。
|
|
198
|
+
6. **不推** tag、不发 npm。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| R1 | `index.ts:613` 工具 execute 把 signal 参数命名 `_signal` 直接丢弃,全仓无 AbortSignal 处理 | scout-0 |
|
|
25
25
|
| R2 | pi 核心 abort 只在批次边界检查 signal,不强杀 in-flight tool promise,等它自然 resolve | scout-1 |
|
|
26
|
-
| R3 | `Orchestrator.collect` 内部 deadline 续期(COLLECT_GRACE_EXTENSIONS=2)+
|
|
26
|
+
| R3 | `Orchestrator.collect` 内部 deadline 续期(COLLECT_GRACE_EXTENSIONS=2)+ 轮询等待点(2.5s 安静窗口 `waitForQuiet`,已由 #232 / PR #244 删除;现存 `waitOutTransientConnection` + `collectArtifactGrowing`)+ applyVerification(600s cap)使 collect 实际可等 3×timeoutMs+600s | scout-0 |
|
|
27
27
|
| R4 | 无显式 stop 命令,LLM 只能逐个 retire | scout-0 |
|
|
28
28
|
|
|
29
29
|
## 冻结决定
|
|
@@ -68,7 +68,7 @@ async execute(_id, params, signal, onUpdate, ctx) {
|
|
|
68
68
|
|
|
69
69
|
- 三个可中断等待点全部接 signal:
|
|
70
70
|
1. `awaitSupervisor` 内 `supervisor.waitSettled(...)` → supervisor 层新增可选 signal 参数:`waitSettled(name, timeoutMs, signal?)`,在 signal abort 时同步 resolve `{ settled:false, abnormal:false, reason:"aborted by parent" }` 并清 timer(`resolveWaiters` 已有清 timer 逻辑,需给单 waiter 提前退出路径——最简单:waiter 注册时若 signal 存在则挂 listener,abort 时从 child.waiters 删除自己 + clearTimeout + resolve)。
|
|
71
|
-
2. `
|
|
71
|
+
2. `waitOutTransientConnection` / `collectArtifactGrowing` 轮询循环:每轮检查 `signal?.aborted` → return。(原文写作 `waitForQuiet`;该方法已由 #232 / PR #244 删除,`signal` 语义不变,仍覆盖 collect 内两个轮询等待点。)
|
|
72
72
|
3. `applyVerification`:`RunVerifyRunner.verify(...)` 若不支持 signal,则在 `finishCollect` 前后不做额外处理(保持现状),但 collect 整体 deadline 检查点加 `signal?.aborted` 短路。
|
|
73
73
|
- `waitSettled` 的 signal 参数是**可选**追加参数,向后兼容(默认 undefined 行为不变)——已有调用点不传即不变。
|
|
74
74
|
|
|
@@ -98,7 +98,7 @@ subagent({action:"stop", all: true}) // 全部 live children
|
|
|
98
98
|
|---|---|---|
|
|
99
99
|
| index.ts | execute signal 接线、waitAction signal 透传、stop action、TOOL_DESCRIPTION | worker |
|
|
100
100
|
| src/extension/runtime.ts | waitOne signal 臂、WaitResult.interrupted、renderWait、wait opts.signal | worker |
|
|
101
|
-
| src/runs/orchestrator.ts | collect opts.signal
|
|
101
|
+
| src/runs/orchestrator.ts | collect opts.signal、轮询等待点/awaitSupervisor 短路 | worker |
|
|
102
102
|
| src/supervisor/rpc-supervisor.ts | waitSettled 可选 signal 参数 | worker |
|
|
103
103
|
| src/supervisor/types.ts | LegionSupervisor 接口 waitSettled 签名(如有) | worker |
|
|
104
104
|
| src/extension/playbook.ts | TOOL_DESCRIPTION / PARENT_PLAYBOOK 同步 stop + wait 说明 | worker |
|
|
@@ -126,7 +126,7 @@ subagent({action:"stop", all: true}) // 全部 live children
|
|
|
126
126
|
- index.ts:595-935(execute、controlAction、launchFamily 分派)
|
|
127
127
|
- index.ts:1765-1810(waitAction)
|
|
128
128
|
- runtime.ts:440-547(ensureCollect/waitOne)、831-838(wait)
|
|
129
|
-
- orchestrator.ts:769-941(collect/awaitSupervisor
|
|
129
|
+
- orchestrator.ts:769-941(collect/awaitSupervisor/轮询等待点/retire;原 `waitForQuiet` 已由 #232 / PR #244 删除)
|
|
130
130
|
- rpc-supervisor.ts:353-396(waitSettled/isAlive)、760-838(retireChild)
|
|
131
131
|
- 4 个单测锚点:`test/unit/supervisor.test.ts:295` 等现有 waitSettled 测试不变(可选第三参数不破坏旧签名)。
|
|
132
132
|
- `waitSettled` 当前在 `src/supervisor/types.ts:81` 接口里,改为 `waitSettled(name: string, timeoutMs?: number, signal?: AbortSignal): Promise<SettleResult>`;唯一非测试调用点 `orchestrator.ts:800`。
|
|
@@ -33,14 +33,14 @@ interrupted (parent turn ended; children still running)
|
|
|
33
33
|
|
|
34
34
|
### D. TUI 提示
|
|
35
35
|
|
|
36
|
-
有 live child 时,aboveEditor 状态栏常驻 `/legion stop [name|all]`(短版 `/legion stop`):railway header 在宽度挤压下可截断摘要以保短 hint;`PI_LEGION_STRIP=tree` 在 header 下另起一行短提示。无 idle 门控。
|
|
36
|
+
有 live child 时,aboveEditor 状态栏常驻 `/legion stop [--force] [name|all]`(短版 `/legion stop`):railway header 在宽度挤压下可截断摘要以保短 hint;`PI_LEGION_STRIP=tree` 在 header 下另起一行短提示。无 idle 门控。
|
|
37
37
|
|
|
38
38
|
## 语义边界
|
|
39
39
|
|
|
40
40
|
| 动作 | children |
|
|
41
41
|
|---|---|
|
|
42
42
|
| Esc 打断 wait / 父 turn | 继续跑;re-arm watch;settle 通知 |
|
|
43
|
-
| `/legion stop` / `stop` action | graceful retire |
|
|
43
|
+
| `/legion stop` / `stop` action | graceful retire; `--force` / `force:true` → hard retire |
|
|
44
44
|
| 主进程退出 / session_shutdown | drain(print/json)+ retire |
|
|
45
45
|
| SIGINT | 进程退出 → retire |
|
|
46
46
|
|