@zhushanwen/pi-subagent-workflow 7.3.4 → 8.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.
Files changed (166) hide show
  1. package/README.md +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -109,6 +109,25 @@ function isTerminal(run: WorkflowRun): boolean {
109
109
  return run.state.status === "done";
110
110
  }
111
111
 
112
+ /**
113
+ * 孤儿 call 判定:dispatch 时捕获的 call 实例是否已不是 calls Map 中该 callId
114
+ * 的当前条目。
115
+ *
116
+ * 为何需要实例级比对(而非只查 run 状态):rebuildRuntime 不改 status(全程
117
+ * running),既有的终态 stale 守卫拦不住旧 runtime 代际的迟到 completion。只有
118
+ * discardInFlightCalls(delete 条目)与新一代 dispatch(set 新实例)会改变
119
+ * 「callId → 实例」映射,故实例不等 ⟺ 本 completion 属于被丢弃/被替换的旧代际
120
+ * (S7-second 竞态:旧失败结果经 postAgentResult 投给新 worker 的同 callId
121
+ * pending,劫持重跑调用为假失败/空串假成功)。
122
+ *
123
+ * 运行期 calls Map 写点仅 discard 的 delete 与 dispatchAgentCall 的 set 两族
124
+ * (jsonl-run-store 的 set 在离线重水合路径,无在飞 promise),正常(非孤儿)
125
+ * 路径下实例恒等,无误判。
126
+ */
127
+ function isOrphanedCall(run: WorkflowRun, callId: number, call: AgentCall): boolean {
128
+ return run.state.calls.get(callId) !== call;
129
+ }
130
+
112
131
  /** 计算第 n 次重试前的退避时间(ms):1s, 2s, 4s 指数。 */
113
132
  function backoffDelay(retryIndex: number): number {
114
133
  return RETRY_BACKOFF_BASE_MS * Math.pow(EXPONENTIAL_BACKOFF_BASE, retryIndex - 1);
@@ -123,6 +142,29 @@ function delay(ms: number): Promise<void> {
123
142
 
124
143
  // ── rebuildRuntime(G3-001 整重建) ─────────────────────────
125
144
 
145
+ /**
146
+ * 移除 run 中未真正完成的在飞 call(status !== "done")及其 trace 节点。
147
+ *
148
+ * 仅 rebuildRuntime 调用——清理被旧 runtime abort 的在飞 call,避免重跑时
149
+ * cached replay 把 abort 产生的 failed 结果当作已完成结果回放(原 MUST_FIX
150
+ * round-4 #1,自 pause 路径移入崩溃重建路径)。genuinely-done 的 call(成功或
151
+ * 失败均 "done")保留,重跑时按原语义 replay(不重复耗 token)。
152
+ *
153
+ * 返回被丢弃的 callId 数组(升序——Map 迭代按插入序,排序保证返回值与
154
+ * rebuildRuntime 的 L3 日志 payload 形态稳定),供调用方记日志。
155
+ */
156
+ function discardInFlightCalls(run: WorkflowRun): number[] {
157
+ const inFlight: number[] = [];
158
+ for (const [callId, call] of run.state.calls) {
159
+ if (call.status !== "done") inFlight.push(callId);
160
+ }
161
+ for (const callId of inFlight) {
162
+ run.state.calls.delete(callId);
163
+ run.state.trace.removeByStepIndex(callId);
164
+ }
165
+ return inFlight.sort((a, b) => a - b);
166
+ }
167
+
126
168
  /**
127
169
  * 重建整个 RunRuntime:新 controller + 新 gate + 新 worker。
128
170
  *
@@ -142,19 +184,56 @@ export function rebuildRuntime(
142
184
  deps: LifecycleDeps,
143
185
  handlers: WorkerHandlers,
144
186
  ): void {
187
+ // OB3(可观察性):rebuild 关键节点 debug 日志——此前函数体 0 处 deps.log,
188
+ // 崩溃自愈只能靠行为证据诊断(L1 入口 / L2 重排 / L3 discard / L4 完成)。
189
+ deps.log?.("debug", "workflow:error-recovery", "runtime rebuild start", {
190
+ runId: run.runId,
191
+ budgetTimeMs: run.spec.budgetTimeMs,
192
+ });
145
193
  const controller = new AbortController();
146
194
  const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
147
195
  const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
148
196
  // D-12 regression fix (round-2 #2):重新调度 run 级墙钟预算计时器。
149
197
  // replaceRuntime 释放旧 runtime 时 clearTimeout 了旧计时器(run-runtime.release),
150
198
  // 新 runtime 必须重排,否则带 budgetTimeMs 的 run 命中一次 worker/script 错误重试后
151
- // 时间预算静默失效(直到 pause/resume 才重排)。deps.scheduleTimeBudget 由 Interface
152
- // 层注入;未注入时(旧测试)跳过重排(兼容,不影响无时间预算的 run)。
153
- const timeBudgetTimer =
154
- run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0 && deps.scheduleTimeBudget
155
- ? deps.scheduleTimeBudget(run.runId, run.spec.budgetTimeMs)
156
- : undefined;
199
+ // 时间预算静默失效(直到 rebuildRuntime 才重排——本函数即唯一重排点)。
200
+ // deps.scheduleTimeBudget 由 Interface 层注入;未注入时(旧测试)跳过重排(兼容,
201
+ // 不影响无时间预算的 run)。
202
+ // 重排分支改为 if——语义与原三元一致(同一条件调 scheduleTimeBudget),仅为在
203
+ // 分支内记 L2 日志,控制流/异常语义零变化。
204
+ let timeBudgetTimer: ReturnType<typeof setTimeout> | undefined;
205
+ if (run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0 && deps.scheduleTimeBudget) {
206
+ timeBudgetTimer = deps.scheduleTimeBudget(run.runId, run.spec.budgetTimeMs);
207
+ deps.log?.("debug", "workflow:error-recovery", "time budget rescheduled", {
208
+ runId: run.runId,
209
+ budgetTimeMs: run.spec.budgetTimeMs,
210
+ });
211
+ }
157
212
  run.replaceRuntime(new RunRuntime(worker, gate, controller, timeBudgetTimer));
213
+ // 清除被旧 runtime abort 的在飞 call——必须在 replaceRuntime 之后同步执行(无
214
+ // await 间隔):replaceRuntime 同步 abort 旧 controller + terminate 旧 worker,
215
+ // 在飞 executeAgentCall 的 finalize 发生在 `await runner.run` resolve 后的
216
+ // microtask,此刻在飞 call 仍为 "running"/"pending"(status !== "done")可精确
217
+ // 清理;genuinely-done 的 call 保留(重跑 replay)。放 delay 退避之前会误删退避
218
+ // 期间自然完成的真结果(重跑重复耗 token);放任何 await 之后,假失败已 finalize
219
+ // 为 "done" 挡不住——重跑 replay 会把 abort 错误当真结果回放,静默污染输出。
220
+ // 注意:discard 只清 Map/trace 条目,旧 executeAgentCall 的 promise 链仍会醒来
221
+ // finalize。markDone 在孤儿实例上无害,但后续投递并非 no-op——postAgentResult
222
+ // 会投给 run.runtime(已是新 worker)的同 callId pending,劫持重跑调用(实测
223
+ // S7-second 竞态:旧失败结果被 worker 侧 resolve 为空串 → 脚本假成功);
224
+ // finalizeCall 的 trace.update 在重跑已 append 同 stepIndex 新节点时命中新节点
225
+ // (瞬时污染,由重跑完成时的 update 覆盖)。该投递由 dispatchAgentCall 的
226
+ // 孤儿守卫(isOrphanedCall)拦截,trace.update 的瞬时污染由 executeAgentCall
227
+ // 的 isOrphaned 谓词(OB2)拦截,此处不重复设防。
228
+ const discardedCallIds = discardInFlightCalls(run);
229
+ deps.log?.("debug", "workflow:error-recovery", "in-flight calls discarded", {
230
+ runId: run.runId,
231
+ callIds: discardedCallIds,
232
+ count: discardedCallIds.length,
233
+ });
234
+ deps.log?.("debug", "workflow:error-recovery", "runtime rebuild complete", {
235
+ runId: run.runId,
236
+ });
158
237
  }
159
238
 
160
239
  // ── handleWorkerMessage(消息路由) ──────────────────────────
@@ -166,7 +245,7 @@ export function rebuildRuntime(
166
245
  * return → transition done,completed(脚本正常返回)
167
246
  * error → handleScriptError(脚本主动抛错)
168
247
  *
169
- * 终态/paused 状态下的 stale 消息丢弃(P0-1)。
248
+ * 终态(done)下的 stale 消息丢弃(P0-1)。
170
249
  */
171
250
  export async function handleWorkerMessage(
172
251
  run: WorkflowRun,
@@ -174,8 +253,8 @@ export async function handleWorkerMessage(
174
253
  deps: LifecycleDeps,
175
254
  handlers: WorkerHandlers,
176
255
  ): Promise<void> {
177
- // 终态/paused 状态丢弃 stale 消息(P0-1)
178
- if (isTerminal(run) || run.state.status === "paused") return;
256
+ // 终态(done)丢弃 stale 消息(P0-1)
257
+ if (isTerminal(run)) return;
179
258
 
180
259
  // M7: 形状校验——防畸形 IPC 消息(worker 崩溃/发非对象)导致下游 TypeError
181
260
  if (typeof raw !== "object" || raw === null) return;
@@ -215,8 +294,12 @@ export async function handleWorkerMessage(
215
294
  * **C-2 修复**:call 完成后检查 `budget.isExceeded` → abortRun(budget_limited),
216
295
  * 终止整个 run(避免烧光预算后继续 spawn 新 call)。
217
296
  *
218
- * **stale 完成守卫**:.then 内 recheck `run.state.status === "running"`,paused 后到达的
219
- * call 完成不写 run.state.calls / postAgentResult(pause 是干净快照)。
297
+ * **stale 完成守卫(两层)**:completion 到达时——
298
+ * 1. `run.state.status === "running"` recheck:run 终止(abort/terminate)后到达的
299
+ * call 完成不写 run.state.calls / 不 postAgentResult(终态快照不被迟到结果污染);
300
+ * 2. 孤儿 call 实例比对(isOrphanedCall):rebuildRuntime 后旧代际 dispatch 的
301
+ * completion 不投递——rebuild 不改 status,第 1 层拦不住跨 runtime 代际的迟到
302
+ * 结果(S7-second 竞态:旧失败结果投给新 worker 劫持重跑 pending → 假成功)。
220
303
  */
221
304
  function dispatchAgentCall(
222
305
  run: WorkflowRun,
@@ -233,7 +316,7 @@ function dispatchAgentCall(
233
316
  return;
234
317
  }
235
318
 
236
- // 已缓存的调用直接 replay(跨 pause/resume)
319
+ // 已缓存的调用直接 replay(跨 rebuild——崩溃重建后重跑脚本,已完成调用按 callId 命中缓存)
237
320
  const cached = run.state.calls.get(msg.callId);
238
321
  if (cached && cached.status === "done") {
239
322
  postAgentResult(run, msg.callId, cached.result!, true);
@@ -313,7 +396,7 @@ function dispatchAgentCall(
313
396
  // runner(runner.run)管 spawn pi 子进程。
314
397
  // assignRuntime/replaceRuntime 保证 status==="running" ⟺ runtime defined,
315
398
  // 故 run.runtime 在此必存在(dispatchAgentCall 仅从 handleWorkerMessage 调用,
316
- // 后者已守 paused/terminal 早期 return)。fallback new AbortController 已移除。
399
+ // 后者已守 terminal(isTerminal)早期 return)。fallback new AbortController 已移除。
317
400
  const runtime = run.runtime!;
318
401
  const signal = runtime.controller.signal;
319
402
  // D-005: onEvent 签名升级——executeAndAwait 直接出 AgentEvent(强类型,
@@ -333,7 +416,10 @@ function dispatchAgentCall(
333
416
  .withSlot(
334
417
  async () => {
335
418
  try {
336
- await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace, onEvent, stream);
419
+ // OB2(S7 残留):isOrphaned 谓词注入——旧代际 finalize 在 trace.update 前被
420
+ // 拦截(判定语义与下方 .then/.catch 守卫同一 isOrphanedCall,详见
421
+ // execute-agent-call.ts finalizeCall 文档注释)。
422
+ await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace, onEvent, stream, () => isOrphanedCall(run, msg.callId, call));
337
423
  } finally {
338
424
  stream?.dispose();
339
425
  }
@@ -343,10 +429,20 @@ function dispatchAgentCall(
343
429
  .then(() => {
344
430
  // 清除 live record:终态已由 executeAgentCall → finalizeCall 写入 node.result,
345
431
  // live 不再需要(且含可变状态,不保留)。无论 stale 与否都清,避免内存泄漏。
346
- // M4: 必须在 stale guard 之前清,否则 pause/resume 循环下 live record 累积。
432
+ // M4: 必须在 stale guard 之前清,否则跨 rebuild 的迟到 completion 会累积 live record
347
433
  node.live = undefined;
348
- // pause/abort 后到达的 stale completion 不写 state(pause 是干净快照)
434
+ // run 终止(终态)后到达的 stale completion 不写 state
349
435
  if (run.state.status !== "running") return;
436
+ // 孤儿 call 守卫(S7-second 竞态):rebuild 的 discardInFlightCalls 已移除本
437
+ // call、或重跑 dispatch 已用新实例替换同 callId 条目时,本 completion 属于旧
438
+ // runtime 代际。postAgentResult 的投递目标是 run.runtime(已是新 worker),
439
+ // 迟到结果会劫持新 worker 内重跑 agent() 的 pending Promise——跳过投递 /
440
+ // budget 同步 / 持久化,仅留日志。executeAgentCall 内 finalizeCall 的
441
+ // trace.update 若已命中重跑新节点(瞬时污染),由重跑完成时的 update 覆盖。
442
+ if (isOrphanedCall(run, msg.callId, call)) {
443
+ deps.log?.("debug", "workflow:error-recovery", "orphan agent call completion dropped", { runId: run.runId, callId: msg.callId });
444
+ return;
445
+ }
350
446
  if (call.result) postAgentResult(run, msg.callId, call.result, false);
351
447
  // D-12 regression fix (round-2 #1):executeAgentCall 内 consume/incrementCallCount
352
448
  // 后同步 worker $BUDGET(否则 $BUDGET.spent()/remaining() 恒为 0)
@@ -401,6 +497,15 @@ function dispatchAgentCall(
401
497
  // Promise 永不 resolve → agent() 永久 await → worker 脚本挂死。构造 failed AgentResult
402
498
  //(与 resolveAgentOpts 失败路径 L262-275 一致的模式)postAgentResult 回 worker,
403
499
  // 让 pending Promise resolve(结果为 error),脚本可继续或失败退出。
500
+ // 孤儿 call 守卫(与 .then 对称,S7-second 竞态):rebuild 后本 call 已被 discard
501
+ // 移除/替换——markDone 虽在孤儿实例上无害,但 trace.update 会污染重跑新建的同
502
+ // stepIndex 节点、postAgentResult 会劫持新 worker 的同 callId pending。孤儿时只
503
+ // 留日志,全部跳过。node.live 无条件先清(旧节点已脱离 trace,防御性统一)。
504
+ node.live = undefined;
505
+ if (isOrphanedCall(run, msg.callId, call)) {
506
+ deps.log?.("debug", "workflow:error-recovery", "orphan agent call failure dropped", { runId: run.runId, callId: msg.callId });
507
+ return;
508
+ }
404
509
  const errorResult: AgentResult = { content: "", error: message };
405
510
  // call 已 done(executeAgentCall 内 finalizeCall 已 markDone)时跳过,避免重复 markDone。
406
511
  // status 理论上必为 running(executeAgentCall L130 markRunning 先于 reject),pending
@@ -411,9 +516,8 @@ function dispatchAgentCall(
411
516
  }
412
517
  // state 一致性三件套(与 resolveAgentOpts 失败 L268-276 / .then L319-325 对等):
413
518
  // trace 标 failed + 清 live record(防泄漏)+ 持久化(catch 恰是最需留证的场景)。
414
- // stale 终态(run 已 paused/done)时 run.runtime 为 undefined,postAgentResult 用
415
- // optional chaining 跳过 worker 回发;trace/state 写入仍执行(无害,pause 快照已存)。
416
- node.live = undefined;
519
+ // stale 终态(run 已 done)时 run.runtime 为 undefined,postAgentResult 用
520
+ // optional chaining 跳过 worker 回发;trace/state 写入仍执行(无害,终态快照已存)。
417
521
  run.state.trace.update(msg.callId, {
418
522
  status: "failed",
419
523
  result: errorResult,
@@ -450,7 +554,7 @@ export function makeSerializeFailedResult(
450
554
  * 异步 postMessage(workflow-result) 回 worker。
451
555
  *
452
556
  * onWorkflowCall 未注入时(向后兼容),返回 error result 让脚本 soft-fail。
453
- * 与 dispatchAgentCall 对称:异步触发(不 await),stale 完成守卫(paused/terminal 不发)。
557
+ * 与 dispatchAgentCall 对称:异步触发(不 await),stale 完成守卫(终态不发)。
454
558
  */
455
559
  function dispatchWorkflowCall(
456
560
  run: WorkflowRun,
@@ -619,9 +723,9 @@ export async function handleWorkerError(
619
723
  deps: LifecycleDeps,
620
724
  handlers: WorkerHandlers,
621
725
  ): Promise<void> {
622
- // 与 handleWorkerMessage 对称——paused/terminal 状态丢弃 stale error。
623
- // 否则 paused 后到达的 worker error 仍会 workerErrorCount++(污染跨 runtime 计数)。
624
- if (isTerminal(run) || run.state.status === "paused") return;
726
+ // 与 handleWorkerMessage 对称——终态(done)丢弃 stale error。
727
+ // 否则终态后到达的 worker error 仍会 workerErrorCount++(污染跨 runtime 计数)。
728
+ if (isTerminal(run)) return;
625
729
 
626
730
  const count = (run.meta.workerErrorCount ?? 0) + 1;
627
731
  run.meta.workerErrorCount = count;
@@ -664,7 +768,7 @@ export async function handleWorkerExit(
664
768
  ): Promise<void> {
665
769
  // G-025: stale exit 事件丢弃(handle 已不是当前 runtime 的 worker)
666
770
  if (!handle.isCurrent) return;
667
- if (isTerminal(run) || run.state.status === "paused") return;
771
+ if (isTerminal(run)) return;
668
772
 
669
773
  if (code === 0) return; // 正常退出,no-op
670
774
 
@@ -695,8 +799,8 @@ export async function handleScriptError(
695
799
  deps: LifecycleDeps,
696
800
  handlers: WorkerHandlers,
697
801
  ): Promise<void> {
698
- // 与 handleWorkerMessage/handleWorkerError 对称——paused/terminal 守卫前置。
699
- if (isTerminal(run) || run.state.status === "paused") return;
802
+ // 与 handleWorkerMessage/handleWorkerError 对称——终态守卫前置。
803
+ if (isTerminal(run)) return;
700
804
 
701
805
  // P2-2: 捕获 worker 诊断日志
702
806
  // L9: 追加而非覆盖
@@ -733,8 +837,8 @@ export async function handleScriptError(
733
837
  /**
734
838
  * 退避后重建 RunRuntime(G3-001 整重建)。
735
839
  *
736
- * 退避期间 run 可能被 pause/abort——rebuildRuntime 前重检状态,paused/terminal 时
737
- * 跳过重建(避免给已暂停的 run 启新 worker)。
840
+ * 退避期间 run 可能被 abort(转终态 done)——rebuildRuntime 前重检状态,终态时
841
+ * 跳过重建(避免给已终止的 run 启新 worker)。
738
842
  */
739
843
  async function scheduleRebuild(
740
844
  run: WorkflowRun,
@@ -749,7 +853,7 @@ async function scheduleRebuild(
749
853
  await delay(backoffDelay(retryIndex));
750
854
 
751
855
  // 退避期间状态可能变化——重检
752
- if (isTerminal(run) || run.state.status !== "running") return;
856
+ if (isTerminal(run)) return;
753
857
 
754
858
  rebuildRuntime(run, deps, handlers);
755
859
  }
@@ -78,13 +78,31 @@ function backoffDelay(retryIndex: number): number {
78
78
  *
79
79
  * 成功:status="completed";失败:status="failed"。
80
80
  * traceNode.stepIndex === call.id(D-10 单源,调用方保证)。
81
+ *
82
+ * 孤儿守卫(OB2,S7 残留):isOrphaned 谓词为 true 时跳过 trace.update——
83
+ * rebuild 竞态窗口中,重跑 dispatch 已 append 同 stepIndex 新节点,旧代际
84
+ * finalize 的 update 会命中新节点,TUI/中间快照短暂可见错误终态。正确性论证:
85
+ * 运行期 calls Map 写点仅 discardInFlightCalls 的 delete 与 dispatchAgentCall 的
86
+ * set 两族(error-recovery.ts isOrphanedCall 文档注释既定),故实例不等 ⟺ 本
87
+ * finalize 属于被丢弃/被替换的旧代际——与 dispatch 层 .then/.catch 守卫
88
+ * (S7-second 修复,8353f6b60)同一判定语义,本守卫只是把它前移到 trace.update
89
+ * 之前。markDone 与 sessionId/sessionFile 同步保留(markDone 在孤儿实例上无害,
90
+ * dispatch 层 catch 路径依赖 call.status 语义)。跳过时不记日志——本文件是纯
91
+ * 函数层无日志通道,dispatch 层 .then 守卫的 orphan completion dropped 日志已
92
+ * 覆盖同一事件的可观察性。
81
93
  */
82
- function finalizeCall(call: AgentCall, result: AgentResult, trace: Trace): void {
94
+ function finalizeCall(
95
+ call: AgentCall,
96
+ result: AgentResult,
97
+ trace: Trace,
98
+ isOrphaned?: () => boolean,
99
+ ): void {
83
100
  call.markDone(result);
84
101
  const status = result.error === undefined ? "completed" : "failed";
85
102
  // 同步 AgentCall 的 sessionId/sessionFile(对齐 trace 节点,持久化 + reset 用)
86
103
  if (result.sessionId !== undefined) call.setSessionId(result.sessionId);
87
104
  if (result.sessionFile !== undefined) call.setSessionFile(result.sessionFile);
105
+ if (isOrphaned?.()) return;
88
106
  trace.update(call.id, {
89
107
  status,
90
108
  result,
@@ -124,6 +142,11 @@ function delay(ms: number): Promise<void> {
124
142
  * @param budget Budget 值对象(consume + isExceeded 检查)
125
143
  * @param signal AbortSignal(runner.run 传播;abort 后不重试)
126
144
  * @param trace Trace 值对象(finalizeCall 时 update)
145
+ * @param onEvent 实时事件回调(live record 更新用)
146
+ * @param stream streaming sink(透传 runner.run)
147
+ * @param isOrphaned 孤儿判定谓词(OB2,可选,默认恒 false):true 时 finalizeCall
148
+ * 跳过 trace.update(判定语义与正确性论证见 finalizeCall 文档注释)。递归重试
149
+ * 透传本谓词。
127
150
  */
128
151
  export async function executeAgentCall(
129
152
  call: AgentCall,
@@ -133,6 +156,7 @@ export async function executeAgentCall(
133
156
  trace: Trace,
134
157
  onEvent?: (event: AgentEvent) => void,
135
158
  stream?: SubagentStream,
159
+ isOrphaned?: () => boolean,
136
160
  ): Promise<void> {
137
161
  call.markRunning();
138
162
 
@@ -145,21 +169,21 @@ export async function executeAgentCall(
145
169
 
146
170
  // stale-context:不重试(P1-5)
147
171
  if (result.error !== undefined && isStaleContextErrorMsg(result.error)) {
148
- finalizeCall(call, result, trace);
172
+ finalizeCall(call, result, trace, isOrphaned);
149
173
  budget.incrementCallCount();
150
174
  return;
151
175
  }
152
176
 
153
177
  // signal 已 abort:调用方终止,不重试(避免无意义的递归)
154
178
  if (signal.aborted) {
155
- finalizeCall(call, result, trace);
179
+ finalizeCall(call, result, trace, isOrphaned);
156
180
  budget.incrementCallCount();
157
181
  return;
158
182
  }
159
183
 
160
184
  // 预算超限:不重试(重试只会突破预算且无意义)
161
185
  if (result.error !== undefined && budget.isExceeded()) {
162
- finalizeCall(call, result, trace);
186
+ finalizeCall(call, result, trace, isOrphaned);
163
187
  budget.incrementCallCount();
164
188
  return;
165
189
  }
@@ -169,15 +193,15 @@ export async function executeAgentCall(
169
193
  await delay(backoffDelay(call.attempts));
170
194
  // 退避期间 signal 可能 abort
171
195
  if (signal.aborted) {
172
- finalizeCall(call, result, trace);
196
+ finalizeCall(call, result, trace, isOrphaned);
173
197
  budget.incrementCallCount();
174
198
  return;
175
199
  }
176
- await executeAgentCall(call, runner, budget, signal, trace, onEvent, stream);
200
+ await executeAgentCall(call, runner, budget, signal, trace, onEvent, stream, isOrphaned);
177
201
  return;
178
202
  }
179
203
 
180
204
  // 终态(成功或达到重试上限的失败)
181
- finalizeCall(call, result, trace);
205
+ finalizeCall(call, result, trace, isOrphaned);
182
206
  budget.incrementCallCount();
183
207
  }