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