@zhushanwen/pi-subagent-workflow 8.11.0 → 8.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "8.11.0",
3
+ "version": "8.13.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -54,20 +54,20 @@
54
54
  ]
55
55
  },
56
56
  "dependencies": {
57
- "@xyz-agent/extension-protocol": "0.9.0",
57
+ "@xyz-agent/extension-protocol": "0.10.0",
58
58
  "@xyz-agent/session-delivery": "0.3.1",
59
59
  "@zhushanwen/pi-ext-guards": "0.3.0",
60
60
  "@zhushanwen/pi-extension-logger": "0.6.0",
61
- "@zhushanwen/subagent-core": "0.8.0",
62
- "@zhushanwen/pi-subagent-cli": "0.1.2",
63
- "@zhushanwen/zcode-subagent-cli": "0.2.0"
61
+ "@zhushanwen/subagent-core": "0.10.0",
62
+ "@zhushanwen/pi-subagent-cli": "0.3.0",
63
+ "@zhushanwen/zcode-subagent-cli": "0.3.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@earendil-works/pi-ai": "^0.84.4",
67
67
  "@earendil-works/pi-coding-agent": "^0.84.4",
68
68
  "@earendil-works/pi-tui": "^0.84.4",
69
69
  "typebox": "*",
70
- "@zhushanwen/pi-pending-notifications": "0.7.0",
70
+ "@zhushanwen/pi-pending-notifications": "0.7.1",
71
71
  "@zhushanwen/pi-structured-output": "5.1.5"
72
72
  },
73
73
  "peerDependenciesMeta": {
@@ -2,12 +2,16 @@
2
2
  //
3
3
  // 三视角:
4
4
  // ①使用者(runtime event-adapter 视角)——帧形状:title=SUBAGENT_INFLIGHT_MARKER、
5
- // options=[JSON 帧](kind/inFlight/sessionId/emittedAt)、控制面级 timeout 在场;
6
- // ②构建者——初始上报(attachSession 触发,kind='initial',无需任何 subagent 调用)
7
- // ack 成功一次后转 'delta';失败折叠 + 延迟重试直至成功一次;推送在途期间
8
- // 多次迁移合并为单帧且携带最新绝对计数;
5
+ // options=[JSON 帧](inFlight/sessionId/emittedAt)、控制面级 timeout 在场;
6
+ // ②构建者——初始上报(attachSession 触发,无需任何 subagent 调用)→ ack 清失败
7
+ // 计数;失败折叠 + 延迟重试;推送在途期间多次迁移合并为单帧且携带最新绝对计数;
9
8
  // ③观察者——onInFlightChanged 同步返回(不 await select,不进生命周期链);
10
9
  // detachSession 停重试,session 死后通道静默。
10
+ //
11
+ // [HISTORICAL] 行为于 9f914b749 变更:kind='initial'|'delta' 帧字段与 initialAcked
12
+ // 状态机作为死面删除(消费方 event-adapter 从不读 kind,绝对计数语义下帧间等价;
13
+ // 该 commit 漏改本文件致断言失效)。断言已对齐现行协议形状——SubagentInFlightReport
14
+ // 无 kind 字段,kind 断言改为 undefined 锁形(防字段悄悄回流)。
11
15
 
12
16
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
17
 
@@ -87,7 +91,7 @@ async function advance(ms: number): Promise<void> {
87
91
  }
88
92
 
89
93
  describe("初始上报(D5:触发时点 = extension 加载完成 / session 就绪)", () => {
90
- it("attachSession 即发 kind='initial' 帧(count=当下快照,无需任何 subagent 调用)", async () => {
94
+ it("attachSession 即发初始帧(count=当下快照,无需任何 subagent 调用)", async () => {
91
95
  const channel = makeSelectChannel();
92
96
  const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
93
97
 
@@ -98,7 +102,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
98
102
  expect(channel.calls[0].title).toBe(SUBAGENT_INFLIGHT_MARKER);
99
103
  expect(channel.calls[0].timeout).toBe(SELECT_TIMEOUT_MS);
100
104
  const frame = parseFrame(channel.calls[0]);
101
- expect(frame.kind).toBe("initial");
105
+ expect(frame.kind).toBeUndefined(); // kind 已删(9f914b749):锁现行协议形状,防字段回流
102
106
  expect(frame.inFlight).toBe(0);
103
107
  expect(frame.sessionId).toBe("sess-u7a");
104
108
  expect(typeof frame.emittedAt).toBe("number");
@@ -107,7 +111,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
107
111
  await advance(0);
108
112
  });
109
113
 
110
- it("初始未送达前发生的迁移不产生第二帧(合并进 initial,送达后转 delta)", async () => {
114
+ it("初始未送达前发生的迁移不产生第二帧(合并为单帧,ack 后补推最新快照)", async () => {
111
115
  const channel = makeSelectChannel();
112
116
  const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS });
113
117
  reporter.attachSession(makeCtx(channel));
@@ -123,7 +127,7 @@ describe("初始上报(D5:触发时点 = extension 加载完成 / session
123
127
  await advance(0);
124
128
  expect(channel.calls).toHaveLength(2);
125
129
  const delta = parseFrame(channel.calls[1]);
126
- expect(delta.kind).toBe("delta");
130
+ expect(delta.kind).toBeUndefined(); // kind 已删(9f914b749):帧间等价,无 initial/delta 之分
127
131
  expect(delta.inFlight).toBe(2); // 补推帧携带最新绝对计数
128
132
  });
129
133
  });
@@ -155,7 +159,7 @@ describe("绝对计数语义(每帧携带当下值,非增量)", () => {
155
159
  });
156
160
 
157
161
  describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)", () => {
158
- it("select resolve undefined(超时/旧版 runtime)→ 折叠重试;重试帧仍 kind='initial';ack 后停", async () => {
162
+ it("select resolve undefined(超时/旧版 runtime)→ 折叠重试;重试帧携带完整快照;ack 后停", async () => {
159
163
  const channel = makeSelectChannel();
160
164
  const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
161
165
  reporter.attachSession(makeCtx(channel));
@@ -168,9 +172,11 @@ describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)"
168
172
  expect(channel.calls).toHaveLength(1); // 未到退避点不重试
169
173
  await advance(1);
170
174
  expect(channel.calls).toHaveLength(2);
171
- expect(parseFrame(channel.calls[1]).kind).toBe("initial"); // 初始「成功一次」未达成
175
+ const retryFrame = parseFrame(channel.calls[1]);
176
+ expect(retryFrame.kind).toBeUndefined(); // kind 已删(9f914b749):重试帧与普通帧等价
177
+ expect(retryFrame.inFlight).toBe(0); // 重试帧携带完整绝对计数快照
172
178
 
173
- // 重试帧得到 ack → 重试停止;随后迁移以 delta 送达
179
+ // 重试帧得到 ack → 重试停止;随后迁移照常送达
174
180
  channel.settle(1, INFLIGHT_REPORT_ACK);
175
181
  await advance(RETRY_MS * 10);
176
182
  expect(channel.calls).toHaveLength(2);
@@ -178,7 +184,7 @@ describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)"
178
184
  reporter.onInFlightChanged();
179
185
  await advance(0);
180
186
  expect(channel.calls).toHaveLength(3);
181
- expect(parseFrame(channel.calls[2]).kind).toBe("delta");
187
+ expect(parseFrame(channel.calls[2]).inFlight).toBe(0);
182
188
  channel.settleAll(INFLIGHT_REPORT_ACK);
183
189
  });
184
190
 
@@ -1,6 +1,6 @@
1
1
  // src/host/inflight-reporter.ts
2
2
  //
3
- // 壳层在途上报出口(u7a,设计权威源:docs/design/crash-forensics-and-watchdog.md
3
+ // 壳层在途上报出口(u7a,设计权威源:docs/architecture/crash-forensics-and-watchdog.md
4
4
  // §3.3 D5「extension 聚合上报」)。本文件属壳侧(shell),对 pi SDK(ExtensionContext
5
5
  // 的 ctx.ui.select 通道)的消费收敛在 host/ 层——core 闭包红线只约束 core(出口回调
6
6
  // 由 core 的 inflight-snapshot 注入,见组合根 index.ts 的 setInFlightListener 接线)。
@@ -15,21 +15,26 @@
15
15
  // ② 事件产生点在 core、上报出口在壳层——本文件即出口(core 零 pi SDK);
16
16
  // ③ marker 路由不广播前端是 u7b(runtime 侧)的约束,本文件不涉及。
17
17
  //
18
+ // 环境门控(2026-09-13 oe-audit + 裸 TUI 闪框事故):上报的消费方是 xyz-agent
19
+ // runtime 的 event-adapter marker 路由,而 runtime spawn pi 恒为 --mode rpc——
20
+ // ctx.mode !== 'rpc' 即无拦截方(裸 pi TUI 下 marker select 会弹真框超时,2026-09-12
21
+ // 实测无限闪框),此时不启动上报(ask-user channel-handler 的 mode 二值判定同款
22
+ // 先例)。非 rpc 会话的 inFlight 判定归 runtime 侧镜像(spawn 预置 0),无信息损失。
23
+ //
18
24
  // 语义(D5):每帧携带**绝对计数**(getInFlightSnapshot,谓词与 core 内
19
- // hasRunningBackground 同源),非增量;初始上报(initial)触发时点 = extension 加载
20
- // 完成——factory 阶段拿不到 ctx/ui(pi 0.84.4 实装:dist/core/extensions/loader.js:463
21
- // await factory(load.api) 只收静态注册面 API(registerTool/events.on…,
22
- // createExtensionAPI :209 函数体无 ui 字段);per-session ctx 由
23
- // dist/core/extensions/runner.js createContext() :503(get ui :508)在事件派发时点
24
- // 构造,emit :624 每次现场建 ctx)——session_start 是 pi 启动序列里最早带 ctx 的钩子
25
- // (dist/core/agent-session.js:1919 _extensionRunner.emit(_sessionStartEvent),默认
26
- // 事件 :152)= 「session 就绪」,即设计所指加载完成时点;不挂任何懒触发(无 subagent
27
- // 的 session 也必上报)。
25
+ // hasRunningBackground 同源),非增量;初始上报触发时点 = extension 加载完成
26
+ // (session_start pi 启动序列里最早带 ctx 的钩子 = 「session 就绪」,即设计所指
27
+ // 加载完成时点;不挂任何懒触发(无 subagent session 也必上报)。
28
28
  //
29
- // 失败语义(D5 缺席语义②):select 失败(超时/通道异常/非确认回包)折叠后**延迟重试
30
- // 直至成功一次**。送达判据 = runtime resolve 的确认回包(INFLIGHT_REPORT_ACK)——
31
- // fire-and-forget resolve(undefined) 与超时不可区分,必须靠显式 ack 区分「已送达」
32
- // 与「旧版 runtime 无路由」,否则 errs 判别(「从未收到上报」只覆盖缺席/旧版)失效。
29
+ // 失败语义(D5 缺席语义②,2026-09-13 oe-audit 修订):select 失败(超时/通道异常/
30
+ // 非确认回包)折叠后延迟重试,**累计 MAX_REPORT_ATTEMPTS 次放弃**(原「重试直至
31
+ // 成功一次」的无界语义为「旧版 runtime」版本错配场景设计——该场景已被同 bundle
32
+ // 发布 + pi 随 runtime 退出销毁两条事实证伪;有界化的防挂死兜底对齐 plugin-bridge
33
+ // MAX_SYNC_ATTEMPTS 形态。session_start 首帧早于 runtime adapter attach 的竞态
34
+ // (R2 实证)在 30×2s=60s 窗口内必然自愈)。送达判据 = runtime resolve 的确认回包
35
+ // (INFLIGHT_REPORT_ACK)——fire-and-forget 下 resolve(undefined) 与超时不可区分,
36
+ // 靠显式 ack 区分「已送达」与「无路由」;放弃后镜像按 absent-report 走 errs 推迟
37
+ // (30min 有界),不丢 errs-safe 兜底。
33
38
 
34
39
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
35
40
  import { SUBAGENT_INFLIGHT_MARKER, isInFlightReportAck } from "@xyz-agent/extension-protocol";
@@ -44,11 +49,16 @@ const SELECT_TIMEOUT_MS = 2_000;
44
49
  /** 失败重试退避(对齐 plugin-bridge SYNC_RETRY_MS 控制面节奏)。 */
45
50
  const RETRY_DELAY_MS = 2_000;
46
51
 
52
+ /** 累计失败放弃上限(对齐 plugin-bridge MAX_SYNC_ATTEMPTS:60s 窗口覆盖 attach 竞态,
53
+ * 有界防 rpc-but-非-xyz orchestrator 场景的永久空转)。 */
54
+ const MAX_REPORT_ATTEMPTS = 30;
55
+
47
56
  /** 在途上报器(组合根 index.ts 持有;per-factory 实例,session_start/shutdown 驱动)。 */
48
57
  export interface InFlightReporter {
49
58
  /**
50
59
  * session_start 注入当前 ctx 并发起初始上报(fire-and-forget——本方法同步返回,
51
- * 不阻塞 session_start 装配链;初始帧 kind='initial',count 为当下绝对计数)。
60
+ * 不阻塞 session_start 装配链;初始帧 count 为当下绝对计数)。非 rpc 模式
61
+ * (ctx.mode !== 'rpc')下为 no-op:无 runtime 拦截方,上报通道不该启动。
52
62
  */
53
63
  attachSession(ctx: ExtensionContext): void;
54
64
  /** session_shutdown 摘除 ctx 并停止重试(session 已死,上报通道随之终结)。 */
@@ -62,6 +72,8 @@ export interface InFlightReporterOpts {
62
72
  selectTimeoutMs?: number;
63
73
  /** 测试注入:重试退避(ms)。缺省 RETRY_DELAY_MS。 */
64
74
  retryDelayMs?: number;
75
+ /** 测试注入:累计失败放弃上限。缺省 MAX_REPORT_ATTEMPTS。 */
76
+ maxAttempts?: number;
65
77
  }
66
78
 
67
79
  /**
@@ -80,19 +92,22 @@ function getSessionId(ctx: ExtensionContext): string | undefined {
80
92
  export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFlightReporter {
81
93
  const selectTimeoutMs = opts.selectTimeoutMs ?? SELECT_TIMEOUT_MS;
82
94
  const retryDelayMs = opts.retryDelayMs ?? RETRY_DELAY_MS;
95
+ const maxAttempts = opts.maxAttempts ?? MAX_REPORT_ATTEMPTS;
83
96
  const logger = getLogger("subagents");
84
97
 
85
98
  // 闭包状态(per-factory 实例;禁模块级 let——同进程多 factory 实例会串台)。
86
99
  let ctx: ExtensionContext | null = null;
87
- /** 初始上报是否已送达(送达后所有帧 kind='delta')。 */
88
- let initialAcked = false;
89
100
  /** 一次推送尝试在途(串行化——绝对计数语义下中间值可安全合并丢弃)。 */
90
101
  let attemptInFlight = false;
91
102
  /** 有待推帧(onInFlightChanged 在推送在途期间置位,成功后立即补推最新值)。 */
92
103
  let dirty = false;
93
104
  /** 重试定时器句柄(单飞;成功/dispose 即清)。 */
94
105
  let retryTimer: ReturnType<typeof setTimeout> | null = null;
95
- /** 首次失败已 warn 留痕(重试循环不刷屏——旧版 runtime 场景会长期重试)。 */
106
+ /** 累计失败次数(成功清零;达 maxAttempts 放弃——本 session 不再重试)。 */
107
+ let failureCount = 0;
108
+ /** 已放弃(累计到顶;session 内终态,成功路径永不触及)。 */
109
+ let givenUp = false;
110
+ /** 首次失败已 warn 留痕(重试循环不刷屏)。 */
96
111
  let firstFailureLogged = false;
97
112
 
98
113
  function clearRetryTimer(): void {
@@ -102,9 +117,9 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
102
117
  }
103
118
  }
104
119
 
105
- /** 推送在途或无 ctx 时仅置脏;否则发起一次尝试(void,不阻塞调用方)。 */
120
+ /** 推送在途、已放弃或无 ctx 时仅置脏;否则发起一次尝试(void,不阻塞调用方)。 */
106
121
  function kick(): void {
107
- if (attemptInFlight || ctx === null) {
122
+ if (attemptInFlight || givenUp || ctx === null) {
108
123
  dirty = true;
109
124
  return;
110
125
  }
@@ -120,11 +135,9 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
120
135
  return;
121
136
  }
122
137
  // 帧内容在发送时刻现取:绝对计数 = 此刻快照(「当前非 idle 句柄数」)、
123
- // 产生时点 = 此刻、kind 按初始上报是否已送达裁决(初始未送达前,脏帧也以
124
- // initial 语义送达——对 errs 判别等价:runtime 收到任何帧即「在场」)。
138
+ // 产生时点 = 此刻(runtime 收到任何帧即「在场」,errs 判别按帧到达置位)。
125
139
  const snapshot = getInFlightSnapshot();
126
140
  const payload = JSON.stringify({
127
- kind: initialAcked ? "delta" : "initial",
128
141
  inFlight: snapshot.inFlight,
129
142
  sessionId: getSessionId(active),
130
143
  emittedAt: Date.now(),
@@ -139,15 +152,25 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
139
152
  }
140
153
  attemptInFlight = false;
141
154
  if (typeof value === "string" && isInFlightReportAck(value)) {
142
- // 送达确认:清重试;初始上报「成功一次」达成后,后续帧一律 delta。
143
- initialAcked = true;
155
+ // 送达确认:清重试与失败计数,补推积压脏帧。
156
+ failureCount = 0;
144
157
  clearRetryTimer();
145
158
  if (dirty && ctx !== null) kick();
146
159
  return;
147
160
  }
148
- // 失败折叠(resolve undefined = 超时/取消/旧版无路由)→ 延迟重试直至成功一次
149
- //(D5 缺席语义②:使「从未收到」只覆盖缺席/旧版形态,不覆盖一次性丢包)。
161
+ // 失败折叠(resolve undefined = 超时/取消/无路由)→ 延迟重试,累计到顶放弃
162
+ //(放弃后镜像按 absent-report 走 errs 推迟,30min 有界,errs-safe 兜底不丢)。
150
163
  logFailure("no ack (timeout, cancelled, or runtime without marker routing)", value);
164
+ failureCount += 1;
165
+ if (failureCount >= maxAttempts) {
166
+ givenUp = true;
167
+ clearRetryTimer();
168
+ logger.warn(
169
+ `[subagent-inflight] in-flight report gave up after ${failureCount} attempts; ` +
170
+ `mirror will treat this session as absent-report (errs-deferred, bounded)`,
171
+ );
172
+ return;
173
+ }
151
174
  if (ctx !== null && retryTimer === null) {
152
175
  retryTimer = setTimeout(() => {
153
176
  retryTimer = null;
@@ -161,7 +184,7 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
161
184
  function logFailure(reason: string, detail: unknown): void {
162
185
  if (!firstFailureLogged) {
163
186
  firstFailureLogged = true;
164
- logger.warn(`[subagent-inflight] in-flight report failed (${reason}); retrying every ${retryDelayMs}ms until acked`, {
187
+ logger.warn(`[subagent-inflight] in-flight report failed (${reason}); retrying every ${retryDelayMs}ms (bounded at ${maxAttempts} attempts)`, {
165
188
  detail: detail instanceof Error ? detail.message : String(detail),
166
189
  });
167
190
  return;
@@ -171,6 +194,15 @@ export function createInFlightReporter(opts: InFlightReporterOpts = {}): InFligh
171
194
 
172
195
  return {
173
196
  attachSession(target: ExtensionContext): void {
197
+ // 环境门控:非 rpc 模式(裸 pi TUI / json / print)无 runtime 拦截方,marker
198
+ // select 会弹真框(2026-09-12 裸 TUI 无限闪框事故根因)——不设 ctx,本 session
199
+ // 全程 no-op。ask-user 的 ctx.mode === "rpc" 二值判定同款先例。
200
+ if (target.mode !== "rpc") return;
201
+ // attach = 新 reporting epoch(新 session / respawn 后重载):放弃态与失败计数
202
+ // 随旧 session 终结,重置重试资格(同一 session 内放弃不恢复——absent-report 兜底)。
203
+ givenUp = false;
204
+ failureCount = 0;
205
+ firstFailureLogged = false;
174
206
  ctx = target;
175
207
  clearRetryTimer();
176
208
  // 初始上报(count=当下绝对计数;session 就绪时点恒为 0——子进程只会在后续
@@ -1,7 +1,7 @@
1
1
  // src/host/pi-host.ts
2
2
  //
3
3
  // pi 壳宿主实现(subagent-core 包抽离 u0-wire)。设计权威源:
4
- // docs/design/subagent-core-package-extraction.md §3.3 D2(含计划期契约细化 3 条)。
4
+ // docs/architecture/subagent-core-package-extraction.md §3.3 D2(含计划期契约细化 3 条)。
5
5
  //
6
6
  // 本文件属壳侧(shell),不进 core 切面——对 pi SDK 与 pi 宿主协作件
7
7
  // (@earendil-works/pi-coding-agent / @zhushanwen/pi-extension-logger /
package/src/index.ts CHANGED
@@ -5,10 +5,13 @@
5
5
  * 注册项:3 tool(subagent + workflow + workflow-script)+ 2 command(subagents + workflows)
6
6
  * + messageRenderer(subagent-bg-notify)+ pi.__workflowRun + session 事件。
7
7
  *
8
- * 三层架构:
9
- * interface/ → 注册胶水(tools/commands/tui
10
- * orchestration/ workflow engine(launcher/lifecycle/error-recovery)
11
- * execution/ → subagents 执行运行时(SubagentService/session-runner/concurrency-pool)
8
+ * 包内结构(执行运行时已迁 packages/subagent-core,本包只留注册面与宿主适配):
9
+ * interface/ → 注册胶水(tools / commands / TUI 渲染 / GUI mappers
10
+ * host/ pi 宿主端口实现(HostServices / NotifyDomain 的 pi 侧兑现)
11
+ * injectors/ → 提示注入器(engine-awareness / model-list / resource-list …)
12
+ * session-lifecycle.ts → 会话生命周期装配 seam(测试可注入 fake 依赖)
13
+ *
14
+ * 架构导航见 docs/extensions/subagents/architecture.md。
12
15
  *
13
16
  * 设计基线:D-004(旧包不动)/ ADR-025(进程内执行)/ D-8(pi.__workflowRun 签名)。
14
17
  */
@@ -26,7 +29,7 @@ import { createPiHostServices, createPiNotifyDomainPorts } from "./host/pi-host.
26
29
  import { createInFlightReporter } from "./host/inflight-reporter.ts";
27
30
 
28
31
  import { bestEffort } from "@zhushanwen/subagent-core";
29
- // ═══ execution/ 层(subagents 核心 + 运行时) ═══
32
+ // ═══ core barrel 消费执行域(执行运行时住 packages/subagent-core) ═══
30
33
  // [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
31
34
  import { syncEnginesFile } from "@zhushanwen/subagent-core";
32
35
  // [W11/DoD#5] registerPiEngine(inproc 'pi' 注册)已随内建引擎删除:registry 'pi'
@@ -53,7 +56,7 @@ import { registerSubagentTool } from "./interface/subagent-tool.ts";
53
56
  import { registerSubagentsCommand } from "./interface/subagents.ts";
54
57
  import { registerWorkflowTool } from "./interface/tool-workflow.ts";
55
58
  import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
56
- // ═══ orchestration/ 层(workflow engine + infra) ═══
59
+ // ═══ core barrel 消费 workflow 域(引擎与 worker 住 packages/subagent-core) ═══
57
60
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
58
61
  import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core";
59
62
  import {
@@ -96,8 +99,11 @@ const logger = getLogger("subagents");
96
99
  //
97
100
  // session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
98
101
  // 触发;sync 子进程(controller 为 undefined,abortRunningControllers 跳过它们)会
99
- // 泄漏为孤儿。process.on 兜底显式 killAllSpawnedChildren 收割全部活子进程。
100
- // guard 防多信号叠加(如 SIGINT 后又 beforeExit)重复 kill。
102
+ // 泄漏为孤儿。process.on 兜底调 killAllSpawnedChildren——[如实口径] core 侧该入口
103
+ // 为镜像置死 no-op(仅清空 core spawnedChildren 镜像记账,不发任何进程信号),真实
104
+ // 回收链 = 子进程 stdin-EOF 自灭(宿主退出 / EngineClient 销毁)+ dispose 链;本调用
105
+ // 保留兜底占位(镜像一致性),不构成真实收割。guard 防多信号叠加(如 SIGINT 后又
106
+ // beforeExit)重复触发。
101
107
  let processShutdownHookFired = false;
102
108
 
103
109
  function reapSpawnedChildrenOnShutdown(): void {
@@ -273,7 +279,18 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
273
279
  scheduleTimeBudget(runId, deps, budgetTimeMs),
274
280
  onWorkflowCall: (name: string, args: Record<string, unknown>, parentRun: WorkflowRun) =>
275
281
  executeNestedWorkflow(name, args, parentRun, deps),
276
- streamSink: getSubagentService()?.getStreamSink() ?? undefined,
282
+ // [H2 W3] workflow agent() 统一派发入口(设计 §3.5):pump 侧 dispatchAgentCall
283
+ // 经此转调 SubagentService.executeWorkflowAgent——真实 record(origin:"workflow"
284
+ // + parentRunId)进 store、共享池/守护/journal 归 service 编排;parentRunId 由
285
+ // pump 补 run.runId。service 单例在 session_start 后必在(run 只能于 session 内
286
+ // 派发);null 时抛错由 pump 的 dispatchCall catch 兜底回发 failed result。
287
+ workflowAgentDispatch: (opts, parentRunId, signal) => {
288
+ const service = getSubagentService();
289
+ if (!service) {
290
+ throw new Error("workflow agent dispatch unavailable: subagent service not initialized");
291
+ }
292
+ return service.executeWorkflowAgent(opts, parentRunId, signal);
293
+ },
277
294
  log,
278
295
  };
279
296
  return deps;
@@ -486,9 +503,11 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
486
503
  // - beforeExit 是退出前最后事件,不 exit(自然退出)。
487
504
  // - idempotent guard(reapSpawnedChildrenOnShutdown 内)防多信号叠加重复 kill。
488
505
  //
489
- // 防线 iii(activate 互斥)已接入:subagent-service.ts 冷路径 resume 调
490
- // acquireActivateLock(含 30s 超时兜底,见 lifecycle-manager.ts ACTIVATE_LOCK_TIMEOUT_MS)。
491
- // 防线 ii(启动 scanOrphanProcesses)骨架就位,启动时接入待实现。
506
+ // 防线 iii(activate 互斥):未接线——acquireActivateLock 机制已随简化清扫删除
507
+ // (历史接线点随协议化重构消失,仅余自持单测)。当前的双写者防护由
508
+ // subagent-service resumesInFlight 集合守卫承担。
509
+ // 防线 ii(启动孤儿扫描):未接线,骨架已随 L2 死代码清扫删除(当前 piped stdio
510
+ // 下 stdin-EOF 自灭链覆盖崩溃路径,见 docs/design/v2-defense-ii-iii-resolution.md)。
492
511
  // ════════════════════════════════════════════════════════════
493
512
  process.on("SIGTERM", () => {
494
513
  reapSpawnedChildrenOnShutdown();
@@ -654,7 +673,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
654
673
  // session_shutdown),Symbol key 单定义点随迁,避免双份定义漂移。
655
674
 
656
675
  // 跨扩展 channel handler 注册入口已收口到 core 深路径
657
- // `@zhushanwen/subagent-core/execution/channel-registry-access.ts`
676
+ // `@zhushanwen/subagent-core/execution/assembly/channel-registry-access.ts`
658
677
  // (getOrCreateChannelRegistry / UiChannelRegistry / ChannelHandler)。
659
678
  // 历史上的包根 re-export 已删:ask-user 等跨扩展消费者经 globalThis 握手
660
679
  // (DIALOG_QUEUE_KEY 同款进程级单例),不再经包根 import 消费本模块。
@@ -1,6 +1,6 @@
1
1
  // engine-awareness 单测([engine-awareness U3])
2
2
  //
3
- // 覆盖(设计 docs/design/subagent-engine-awareness-injection.md 验收挂钩 D1/D1b/D2/D3/D5):
3
+ // 覆盖(设计 docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)验收挂钩 D1/D1b/D2/D3/D5):
4
4
  // 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,直连导入)
5
5
  // 2. buildEngineChangeNotice:§3.1 文案骨架、pi/非 pi 指路段分界、不含任何模型清单(D4)
6
6
  // 3. runEngineAwarenessTurn 编排:
@@ -1,6 +1,6 @@
1
1
  // engine-section-stability.test.ts —— [engine-awareness U4 stability-guard]
2
2
  //
3
- // 字节稳定守护(设计 docs/design/subagent-engine-awareness-injection.md §3.3 D7 /
3
+ // 字节稳定守护(设计 docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)§3.3 D7 /
4
4
  // §4 A8 的代码级前置):engine 段延续注入纪律——确定性渲染、链尾位置,引擎切换只断
5
5
  // system prompt 尾部 cache 前缀(与 provider models 段变更同判,不新增 cache 破坏面,
6
6
  // cache-probe 前缀指纹归因兼容)。
@@ -110,9 +110,6 @@ function fakeEngine(id: string, models: Array<{ id: string; name?: string }>): E
110
110
  run: async () => {
111
111
  throw new Error("not used in this test");
112
112
  },
113
- interact: async () => {
114
- throw new Error("not used in this test");
115
- },
116
113
  read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
117
114
  // 每次调用返回新数组实例(模拟 registry 每 turn 现值)——渲染必须与实例无关
118
115
  listModels: () => models.map((m) => ({ ...m })),
@@ -1,7 +1,7 @@
1
1
  // src/injectors/engine-awareness.ts
2
2
  //
3
3
  // [engine-awareness U3] 引擎感知检测编排。设计权威源:
4
- // docs/design/subagent-engine-awareness-injection.md §2.3 物理数据流 /
4
+ // docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)§2.3 物理数据流 /
5
5
  // §3.3 D1(per-turn poll)、D1b(lastEngine 初始化与静默基线化)、D2(reload 先行)、
6
6
  // D3(通知通道)、D5(三态读取语义)。
7
7
  //
@@ -14,7 +14,7 @@
14
14
  * core barrel(渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);
15
15
  * guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
16
16
  * - U11(sink 设计):装配循环(发现→解析→去重→排序 + warn/error 口径)整体改
17
- * 消费 core discoverAgents(execution/agents-assembly,U2/A6)——经工厂 assemble
17
+ * 消费 core discoverAgents(execution/assembly/agents-assembly,U2/A6)——经工厂 assemble
18
18
  * 覆写槽委托,壳侧收缩为「宿主注入根现取 + 委托」,语义等值口径见
19
19
  * agents-assembly.ts 头注(⛔1 探针对照注入 XML diff 为空验收)。
20
20
  *
@@ -167,7 +167,7 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
167
167
  "..",
168
168
  );
169
169
  const actionsSrc = readFileSync(
170
- join(coreRoot, "src/execution/subagent-actions-core.ts"),
170
+ join(coreRoot, "src/execution/assembly/subagent-actions-core.ts"),
171
171
  "utf-8",
172
172
  );
173
173
  // 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
@@ -22,11 +22,12 @@
22
22
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
23
23
 
24
24
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
25
- import { abortRun } from "@zhushanwen/subagent-core";
25
+ import { abortRun, getSubagentService } from "@zhushanwen/subagent-core";
26
26
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
27
27
  import { parseWorkflowRpcCommand, type WorkflowRpcAction } from "./command-actions.ts";
28
28
  import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
29
29
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
30
+ import { LIST_LIMIT } from "./list-shared.ts";
30
31
 
31
32
  /** runId 截断长度(显示用)。 */
32
33
  const RUNID_SHORT = 8;
@@ -248,6 +249,12 @@ function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
248
249
  *
249
250
  * ViewActions 通过 deps 调 lifecycle(abort),与 view 解耦——
250
251
  * view 单测可注入 mock actions(见 views/__tests__/WorkflowsView-signature.test.ts)。
252
+ *
253
+ * [H2 W3] live 进度数据源(设计 D2 进度源切换):view 经 store 查询
254
+ * collectRecordsByParentRunId(run.runId)(内存 ∪ 磁盘重建 ∪ manifest,LIST_LIMIT
255
+ * 口径)配对 running trace node 渲染实时进度。service 单例未就绪(理论上 run 只能
256
+ * 于 session 内存在,session_start 后必在;防御分支)时不注入——view 走终态
257
+ * result 渲染路径。
251
258
  */
252
259
  async function openView(
253
260
  run: WorkflowRun,
@@ -256,7 +263,11 @@ async function openView(
256
263
  deps: LauncherDeps,
257
264
  ): Promise<void> {
258
265
  const actions: ViewActions = {
259
- abort: (runId: string) => abortRun(runId, deps),
266
+ abort: (runId) => abortRun(runId, deps),
260
267
  };
261
- await createWorkflowsView(run, theme, ctx, actions, deps.store.stateFilePath(run.runId));
268
+ const service = getSubagentService();
269
+ const liveRecords = service
270
+ ? () => service.queries.collectRecordsByParentRunId(run.runId, LIST_LIMIT)
271
+ : undefined;
272
+ await createWorkflowsView(run, theme, ctx, actions, deps.store.stateFilePath(run.runId), liveRecords);
262
273
  }
@@ -138,16 +138,24 @@ export function shortId(id: string): string {
138
138
  * running → { icon: undefined, color: "accent" }
139
139
  * icon 留空是因为 running 的 spinner 需 seed 驱动,
140
140
  * 调用方用 detailsSeed(details) 算 seed 后调 spinnerGlyph(seed).
141
- * done → { "✓", "success" }
142
- * failed → { "✗", "error" }
143
- * cancelled { "■", "muted" }
141
+ * idle → { "✓", "success" }
142
+ *
143
+ * [U2 两态迁移] 永久会话模型(subagent-permanent-session-model §3.2.2)状态机收敛为
144
+ * running | idle 两态,终态概念删除——旧 closed 分支(v4 B-1 统一终态 ✓ success 色)
145
+ * 迁移为 idle(已收口/等续聊,语义上「上一轮有产出」保留 ✓ success 色;stopReason
146
+ * 派生词的渲染细化归 U8 投影面)。签名兼容 BgNotifyRecord 的通知词汇 "closed"
147
+ * (通知文案载体的本地类型不受 ExecutionStatus 两态约束,桥接映射同 idle)。
144
148
  */
145
- export function statusGlyph(status: ExecutionStatus): { icon: string | undefined; color: string } {
149
+ export function statusGlyph(
150
+ status: ExecutionStatus | "closed",
151
+ ): { icon: string | undefined; color: string } {
146
152
  switch (status) {
147
153
  case "running":
148
154
  return { icon: undefined, color: "accent" };
155
+ case "idle":
149
156
  case "closed":
150
- // v4 B-1: closed 统一终态(含 cancelled)。默认 ✓ success 色。
157
+ // 已收口/等续聊(closed = BgNotifyRecord 通知词汇桥接)。默认 ✓ success
158
+ // (承接旧 closed 分支的视觉基线)。
151
159
  return { icon: "✓", color: "success" };
152
160
  }
153
161
  }
@@ -494,6 +502,22 @@ export function formatElapsed(startedAt?: string, now: number = Date.now()): str
494
502
  return formatElapsedSeconds(Math.max(0, secs));
495
503
  }
496
504
 
505
+ /**
506
+ * Run status 行的 elapsed([H2 A2] done 后冻结)。
507
+ *
508
+ * 与 formatElapsed 的差异仅在 now 基准:completedAt 有值(run 已终态)时以
509
+ * completedAt 为基准 → elapsed 恒等于 completedAt - startedAt,不再随墙钟增长;
510
+ * running(completedAt 缺省)沿用 Date.now() 缺省实时跳动。时钟异常
511
+ * (completedAt < startedAt)由 formatElapsed 钳 0 兜底("0s")。
512
+ */
513
+ export function formatRunStatusElapsed(
514
+ startedAt?: string,
515
+ completedAt?: string,
516
+ now: number = Date.now(),
517
+ ): string {
518
+ return formatElapsed(startedAt, completedAt ? new Date(completedAt).getTime() : now);
519
+ }
520
+
497
521
  /**
498
522
  * Format a live eventLog entry(live 路径 Activity 区用)。
499
523
  *
@@ -48,12 +48,16 @@ function isFailedStatus(s: string): boolean {
48
48
  * 把 workflow/subagent 状态字符串映射到 list-tree 的三态 status。
49
49
  *
50
50
  * 输入可能是纯 RunStatus(running/done)、RunStatus+reason 组合
51
- * (如 "done (failed)"),或 subagent status(running/done/failed/cancelled/crashed)。
51
+ * (如 "done (failed)"),或 subagent status(running/idle/legacy done/failed/
52
+ * cancelled/crashed)。
52
53
  *
53
54
  * 映射规则:
54
55
  * - running → running
55
56
  * - failed / aborted / error / crashed / cancelled / budget_limited / time_limited → failed
56
- * - 其他(done / completed / success / pending)→ done
57
+ * - 其他(done / completed / success / pending / idle)→ done
58
+ *
59
+ * [U8 两态] subagent 的 idle(无任务在飞可续聊)在协议三态里落 done(空闲 = 无
60
+ * 进行中工作,check 图标);「为什么停」不进树形状态(协议无该维度)。
57
61
  */
58
62
  export function mapRunStatus(status: string): TreeStatus {
59
63
  const s = status.toLowerCase();
@@ -602,12 +602,18 @@ export class SubagentsListComponent implements Component {
602
602
  }
603
603
  }
604
604
 
605
- /** 元数据段:id/mode/status/turns/tokens 行 + model/thinking 行 + parent/children 层级行。 */
605
+ /** 元数据段:id/mode/status/stopReason/turns/tokens 行 + model/thinking 行 + parent/children 层级行。 */
606
606
  private pushMetaSection(content: string[], record: SubagentRecord, width: number): void {
607
607
  const t = this.theme;
608
- // 元数据:第 1 行 id + 状态 + turns + tokens
608
+ // 元数据:第 1 行 id + 状态 + turns + tokens。stopReason(上一轮为什么停)只在
609
+ // 详情面板出现(§3.2.1 展示层词汇收敛:列表主展示用状态词,停因是排障信息);
610
+ // 有值即追加 `stopped: <reason>` 段(与 SubagentTab 同判据——A-lite 轮终
611
+ // running-resumable 也携带合法停因),缺省省略(在飞首轮 / 存量无字段)。
612
+ const stopPart = record.stopReason
613
+ ? ` · stopped: ${record.stopReason}`
614
+ : "";
609
615
  content.push(truncLine(
610
- t.fg("dim", `${record.id} · ${record.mode} · ${record.status} · ${record.turns} turns · ${formatTokens(record.totalTokens)}`),
616
+ t.fg("dim", `${record.id} · ${record.mode} · ${record.status}${stopPart} · ${record.turns} turns · ${formatTokens(record.totalTokens)}`),
611
617
  width,
612
618
  ));
613
619
  // 元数据:第 2 行 model + thinking(括号分组)
@@ -17,7 +17,7 @@
17
17
  // └────────────────────────────┘ ← overlay 底空白行
18
18
  // (外层框线仅为示意,实际是空白行/空格列)
19
19
  //
20
- // 契约(ctx.ui.custom overlay,对照 pi-tui-development-guide.md §3.2):
20
+ // 契约(ctx.ui.custom overlay,对照 tui-rendering-pitfalls.md §3.2):
21
21
  // custom<void>((tui, theme, kb, done) => Component, {overlay:true, overlayOptions})
22
22
  // Component: render(width):string[] + invalidate() + handleInput?(data)
23
23
  //
@@ -3,7 +3,7 @@
3
3
  // subagent tool 六 action 的 pi adapter 壳(sink 设计 D6② 消费收缩)。
4
4
  //
5
5
  // 领域内核(入参校验 / message 拒绝文案分流 / fork-from 拒绝链 / 终态映射 /
6
- // list 投影)已下沉 core @zhushanwen/subagent-core/execution/subagent-actions-core——
6
+ // list 投影)已下沉 core @zhushanwen/subagent-core/execution/assembly/subagent-actions-core——
7
7
  // ⛔4 行为快照等值锚定见 core __tests__/subagent-actions-core.test.ts(期望值硬编码自
8
8
  // 本文件迁移前实测输出,含错误文案逐字锚)。本壳按设计仅保留三类职责:
9
9
  // 1. core 调用面 re-export:六 handler + 领域类型(签名不变,消费方零改动)