@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
@@ -3,11 +3,12 @@
3
3
  *
4
4
  * Workflow run 生命周期 free functions(D-12)。
5
5
  *
6
- * 4 个导出函数:
6
+ * 5 个导出函数:
7
7
  * - runWorkflow(spec, deps, signal?) → Promise<runId>
8
- * - pauseRun(runId, deps) → Promise<void>(A4 原子性)
9
- * - resumeRun(runId, deps) → Promise<void>(G3-001 整重建)
10
- * - abortRun(runId, deps, reason?) → Promise<void>(done no-op)
8
+ * - abortRun(runId, deps, reason?, doneReason?) → Promise<void>(done no-op)
9
+ * - terminateRunningRuns(deps, reason) → Promise<void>(session 切换/关闭终止)
10
+ * - evictDoneRunsBeyondCap(runs, keepDone) → number(done run 内存淘汰)
11
+ * - scheduleTimeBudget(runId, deps, budgetTimeMs) → timer(C.7 时间预算)
11
12
  *
12
13
  * 私有 makeHandlers(run, deps) → WorkerHandlers:
13
14
  * - onMessage → handleWorkerMessage(run, raw, deps, handlers)
@@ -15,12 +16,14 @@
15
16
  * - onExit(code, handle) → handleWorkerExit(run, code, handle, deps, handlers)
16
17
  * (G-025:handle.isCurrent 检查内化在 handleWorkerExit 内)
17
18
  *
18
- * **A4 原子性**:pause/abort 内部 transition 先 releaseRuntime(cleanup before mutate),
19
- * 失败时 status 不变。transition("paused"/"done") 在 WorkflowRun.transition 内已实现
19
+ * **A4 原子性**:abort/terminate 内部 transition 先 releaseRuntime(cleanup before
20
+ * mutate),失败时 status 不变。transition("done") 在 WorkflowRun.transition 内已实现
20
21
  * 「releaseRuntime → 改 status」原子顺序。
21
22
  *
22
- * **G3-001**:pause 时整个 RunRuntime 丢弃(AbortController 一次性,无法复用);
23
- * resume 时 assignRuntime 重建 worker/gate/controller。
23
+ * **G3-001**(run 一次性生命周期):AbortController 一次性无法复用,runtime 释放后
24
+ * 只有两类重建——rebuildRuntime(error-recovery,崩溃重试路径,run 保持 running、
25
+ * replaceRuntime 原子换新)与 abort/terminate 的终态释放(transition("done") 内
26
+ * releaseRuntime,run 不再恢复)。
24
27
  *
25
28
  * **D-13**:maxConcurrency=4(ConcurrencyGate 默认值)。
26
29
  *
@@ -57,6 +60,15 @@ const RUNID_RADIX = 36;
57
60
  const RUNID_SLICE_START = 2;
58
61
  const RUNID_SLICE_END = 8;
59
62
 
63
+ /**
64
+ * done run 内存保留窗口(K=20)。
65
+ *
66
+ * 本淘汰是 done run 内存有界性的唯一来源:calls.result 不裁、单聚合大小不随 wave1
67
+ * 裁剪缩小,故内存上限 = K × 实际聚合大小。同时定义 actionStatus 可查刚完成 run
68
+ * 的窗口(超出窗口的 done run 不再出现在列表中——已接受的用户可见变化)。
69
+ */
70
+ export const MAX_RETAINED_DONE_RUNS = 20;
71
+
60
72
  function generateRunId(): string {
61
73
  return `wf-${Date.now()}-${Math.random().toString(RUNID_RADIX).slice(RUNID_SLICE_START, RUNID_SLICE_END)}`;
62
74
  }
@@ -103,11 +115,10 @@ function makeHandlers(run: WorkflowRun, deps: LifecycleDeps): WorkerHandlers {
103
115
  /**
104
116
  * 启动 run 级墙钟时间预算计时器:到期后 abortRun(doneReason="time_limited")。
105
117
  *
106
- * 恢复旧 orchestrator-budget.ts 的 scheduleTimeBudgetCheck 语义——run/resume 各
107
- * 启一个 setTimeout(maxTimeMs),到期若 run 仍 running/paused 则转 done,time_limited。
108
- * 计时器存入 RunRuntime.timeBudgetTimer,release(pause/abort/replaceRuntime)时
109
- * 自动清理,避免孤儿触发。resume 会调度全新计时器(与旧语义一致:pause+resume
110
- * 重置墙钟预算)。
118
+ * 恢复旧 orchestrator-budget.ts 的 scheduleTimeBudgetCheck 语义——runWorkflow 启动
119
+ * 一个 setTimeout(maxTimeMs),到期若 run 仍未终态则转 done,time_limited。
120
+ * 计时器存入 RunRuntime.timeBudgetTimer,release(abort/replaceRuntime)时
121
+ * 自动清理,避免孤儿触发。worker/script 错误重试经 rebuildRuntime 重排新计时器。
111
122
  *
112
123
  * @returns 计时器句柄(未设预算时 undefined)
113
124
  */
@@ -134,10 +145,12 @@ export function scheduleTimeBudget(
134
145
  /**
135
146
  * 启动一个 workflow run。
136
147
  *
137
- * 流程:创建 WorkflowRun(paused)+ makeHandlers + 构建 RunRuntime(worker+gate+controller)
138
- * + assignRuntime(paused → running)+ 注册到 deps.runs + store.save。
148
+ * 流程:创建 WorkflowRun(running,I1 构造期跳过)+ makeHandlers + 构建 RunRuntime
149
+ * (worker+gate+controller)+ assignRuntime(注入 runtime,恢复 I1)+ 注册到
150
+ * deps.runs + store.save。
139
151
  *
140
- * @param spec RunSpec(不可变输入,含 scriptSource/args)
152
+ * @param spec RunSpec(scriptSource 只读;args 会被原地注入 _runId——rfl C2 契约,
153
+ * worker 启动与崩溃重建共用同一 args 对象)
141
154
  * @param deps LifecycleDeps(store/workerHost/runner/runs)
142
155
  * @param signal 外部 abort signal(可选;abort 时调 abortRun)
143
156
  * @returns runId(wf-<timestamp>-<random>)
@@ -150,11 +163,19 @@ export async function runWorkflow(
150
163
  ): Promise<string> {
151
164
  // m3 E9:参数校验单一 chokepoint,钉在所有副作用前(generateRunId/log/signal
152
165
  // listener/runs.set/workerHost.start/store.save/pending:register)。校验失败时
153
- // zero side effects。coerceTypes 原地规范化 spec.args——worker 启动与 pause/resume
154
- // 重建共用同一对象(run.spec === spec),恢复路径参数一致。
166
+ // zero side effects。coerceTypes 原地规范化 spec.args——worker 启动与崩溃重建
167
+ // 共用同一对象(run.spec === spec),恢复路径参数一致。
155
168
  validateRunArgs(spec);
156
169
 
157
170
  const runId = generateRunId();
171
+ // rfl 仪表(tier-1 §7.1):注入稳定 _runId。runAndWait 与 executeNestedWorkflow
172
+ // 两个 args 入口都经本 choke point;rebuildRuntime 复用 run.spec.args 同一对象
173
+ // (error-recovery.ts),worker rebuild 后脚本侧 $ARGS._runId 不漂移——修复
174
+ // 「rebuild 回退 run-<Date.now()> 导致同一逻辑 run 碎裂到多个 state 目录」。
175
+ // 注入在 validateRunArgs 之后,不参与脚本参数 schema 校验(引擎内部字段)。
176
+ if (spec.args && typeof spec.args === "object") {
177
+ spec.args._runId = runId;
178
+ }
158
179
  deps.log?.("debug", "workflow:lifecycle", "runWorkflow start", { runId, scriptName: spec.scriptName });
159
180
 
160
181
  // P1-2: pre-aborted signal → fail fast
@@ -166,7 +187,7 @@ export async function runWorkflow(
166
187
  runId,
167
188
  spec,
168
189
  {
169
- status: "paused",
190
+ status: "running",
170
191
  budget: spec.budgetRef ?? new Budget({
171
192
  maxTokens: spec.budgetTokens,
172
193
  maxTimeMs: spec.budgetTimeMs,
@@ -192,9 +213,6 @@ export async function runWorkflow(
192
213
  );
193
214
  }
194
215
 
195
- // 注册到 deps.runs(makeHandlers + assignRuntime 需要 run 已在 map)
196
- deps.runs.set(runId, run);
197
-
198
216
  // 构造 handlers + runtime(worker + gate + controller)
199
217
  const handlers = makeHandlers(run, deps);
200
218
  const controller = new AbortController();
@@ -207,9 +225,14 @@ export async function runWorkflow(
207
225
  : undefined;
208
226
  const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
209
227
 
210
- // assignRuntime(paused → running,原子绑定 runtime + status)
228
+ // assignRuntime(注入 runtime,恢复 I1:running ⟺ runtime!==undefined)
211
229
  run.assignRuntime(runtime);
212
230
 
231
+ // 注册到 deps.runs(assignRuntime 之后——构造到 assignRuntime 之间 run 处于
232
+ // I1 跳过窗口(running 而 runtime undefined),后移保证窗口对外不可见;
233
+ // worker.start 抛错时 run 未注册,无孤儿 run 残留)
234
+ deps.runs.set(runId, run);
235
+
213
236
  await deps.store.save(run);
214
237
  deps.log?.("debug", "workflow:lifecycle", "run saved", { runId, status: run.state.status });
215
238
 
@@ -226,112 +249,10 @@ export async function runWorkflow(
226
249
  return runId;
227
250
  }
228
251
 
229
- // ── pauseRun ─────────────────────────────────────────────────
230
-
231
- /**
232
- * 暂停 running workflow。
233
- *
234
- * **A4 原子性**:WorkflowRun.transition("paused") 内部先 releaseRuntime(cleanup
235
- * before mutate),再改 status。releaseRuntime 失败时 status 不变(仍 running)。
236
- *
237
- * **G3-001**:transition("paused") 调 releaseRuntime,整个 RunRuntime 丢弃
238
- * (runtime=undefined)。AbortController 一次性无法复用。
239
- *
240
- * @throws runId 不存在 / status !== "running"
241
- */
242
- export async function pauseRun(runId: string, deps: LifecycleDeps): Promise<void> {
243
- const run = deps.runs.get(runId);
244
- if (!run) {
245
- throw new Error(`Workflow '${runId}' not found`);
246
- }
247
- if (run.state.status !== "running") {
248
- throw new Error(
249
- `Cannot pause workflow in state '${run.state.status}': only 'running' can be paused`,
250
- );
251
- }
252
-
253
- // A4: transition 内部 releaseRuntime(cleanup before mutate)
254
- run.transition("paused");
255
-
256
- // MUST_FIX (round-4 #1): 清理被 abort 的在飞 call(status !== "done")。
257
- // transition 已同步 abort controller + terminate worker,但 in-flight 的
258
- // executeAgentCall 会在后续 microtask 被 finalizeCall 标记为 "done"(带 abort
259
- // 错误结果)。若保留,resume 时 dispatchAgentCall 的 cached 路径会把该 failed
260
- // 结果原样 replay,静默污染 workflow 输出。此处同步移除在飞 call + 其 trace 节点,
261
- // 让 resume 重发 agent-call 走全新执行路径。
262
- //
263
- // 同步执行(在 await store.save 前):transition 与本清理间无 await,微任务无法
264
- // 插入,此时在飞 call 仍为 "running"/"pending",可精确清理;genuinely-done 的
265
- // call 不受影响(resume 时正常 replay)。后续 finalizeCall 在已移除的 orphan call
266
- // 上运行(markDone 无外部副作用),trace.update 因节点已移除为 no-op。
267
- discardInFlightCalls(run);
268
-
269
- await deps.store.save(run);
270
- }
271
-
272
- /**
273
- * 移除 run 中未真正完成的在飞 call(status !== "done")及其 trace 节点。
274
- *
275
- * 仅 pauseRun 调用——清理被 abort 的在飞 call,避免 resume 时 cached replay
276
- * 把 abort 产生的 failed 结果当作已完成结果回放(MUST_FIX round-4 #1)。
277
- * genuinely-done 的 call(成功或失败均 "done")保留,resume 时按原语义 replay。
278
- */
279
- function discardInFlightCalls(run: WorkflowRun): void {
280
- const inFlight: number[] = [];
281
- for (const [callId, call] of run.state.calls) {
282
- if (call.status !== "done") inFlight.push(callId);
283
- }
284
- for (const callId of inFlight) {
285
- run.state.calls.delete(callId);
286
- run.state.trace.removeByStepIndex(callId);
287
- }
288
- }
289
-
290
- // ── resumeRun ────────────────────────────────────────────────
291
-
292
- /**
293
- * 恢复 paused workflow。
294
- *
295
- * **G3-001**:assignRuntime 重建 worker/gate/controller + transition running。
296
- * worker 重跑脚本,已完成调用从 RunState.calls replay(callCache 在 calls Map 里)。
297
- *
298
- * **A4 mirror**:先 startWorker(副作用可能抛),成功后才 assignRuntime 改 status。
299
- * 若 Worker 构造抛错,workflow 保持 paused,调用方可重试。
300
- *
301
- * @throws runId 不存在 / status !== "paused"
302
- */
303
- export async function resumeRun(runId: string, deps: LifecycleDeps): Promise<void> {
304
- const run = deps.runs.get(runId);
305
- if (!run) {
306
- throw new Error(`Workflow '${runId}' not found`);
307
- }
308
- if (run.state.status !== "paused") {
309
- throw new Error(
310
- `Cannot resume workflow in state '${run.state.status}': only 'paused' can be resumed`,
311
- );
312
- }
313
-
314
- // A4 mirror: 先 startWorker(副作用),成功后才 assignRuntime
315
- const handlers = makeHandlers(run, deps);
316
- const controller = new AbortController();
317
- const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
318
- const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
319
- // C.7:resume 重新调度时间预算计时器(与 run 对称;旧 pause 已清旧计时器)。
320
- const timeBudgetTimer =
321
- run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0
322
- ? scheduleTimeBudget(runId, deps, run.spec.budgetTimeMs)
323
- : undefined;
324
- const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
325
-
326
- // assignRuntime(paused → running,原子绑定 runtime + status)
327
- run.assignRuntime(runtime);
328
- await deps.store.save(run);
329
- }
330
-
331
252
  // ── abortRun ─────────────────────────────────────────────────
332
253
 
333
254
  /**
334
- * 中止 workflow(running 或 paused)。
255
+ * 中止 workflow(running)。
335
256
  *
336
257
  * **done 状态 no-op**:已终态的 run 不重复 abort。
337
258
  * **A4 原子性**:transition("done", doneReason) 内部先 releaseRuntime。
@@ -361,13 +282,6 @@ export async function abortRun(
361
282
  return;
362
283
  }
363
284
 
364
- // 只允许 running/paused abort(防御)
365
- if (run.state.status !== "running" && run.state.status !== "paused") {
366
- throw new Error(
367
- `Cannot abort workflow in state '${run.state.status}': only 'running' or 'paused' can be aborted`,
368
- );
369
- }
370
-
371
285
  // 记录中止原因
372
286
  if (reason) {
373
287
  run.state.error = reason;
@@ -382,3 +296,101 @@ export async function abortRun(
382
296
  deps.log?.("debug", "workflow:lifecycle", "emit pending:unregister done", { runId });
383
297
  deps.onRunDone?.(run);
384
298
  }
299
+
300
+ // ── terminateRunningRuns(session 切换/关闭:终止全部 running run) ────────
301
+
302
+ /**
303
+ * 终止 deps.runs 中全部 running run(session 切换 / session 关闭时调用)。
304
+ *
305
+ * 一次性生命周期(D-2):session 离开当刻,running run 的 token 投入作废,转
306
+ * done,failed 持久化落盘——重启后 kill-9 恢复不误判,也不再存在「挂起待恢复」
307
+ * 的中间态。
308
+ *
309
+ * per-run 行为:`state.error = reason` → `transition("done","failed")`(内部先
310
+ * releaseRuntime,A4)→ `await store.save(run)` → `eventBus.emit("pending:unregister",
311
+ * {reason:"failed"})`。
312
+ *
313
+ * **不调 deps.onRunDone**:对齐 session_start 恢复先例(index.ts kill-9 恢复只发
314
+ * unregister、不发 onRunDone)——session 切换/关闭语境下主 agent 已离开本 session,
315
+ * 注入完成通知只会把消息发给已离开的 session。
316
+ *
317
+ * **不调 discardInFlightCalls**:run 已转终态不再 replay(无恢复路径),在飞 call
318
+ * 缓存清不清都不影响结果;该清理仅 rebuildRuntime 需要(崩溃重试会重放脚本,
319
+ * 假失败结果会污染重跑输出)。
320
+ *
321
+ * 单 run 失败(try/catch + log 带 runId/reason)不中断其余 run——终止是批量收尾,
322
+ * 一个 run 落盘失败不应放走其余 run 的 failed 状态。
323
+ *
324
+ * @param deps LifecycleDeps(runs/store/eventBus/log)
325
+ * @param reason 终止原因(写入 run.state.error,如 "Session switched: run terminated")
326
+ */
327
+ export async function terminateRunningRuns(
328
+ deps: LifecycleDeps,
329
+ reason: string,
330
+ ): Promise<void> {
331
+ for (const run of deps.runs.values()) {
332
+ if (run.state.status !== "running") continue;
333
+ try {
334
+ deps.log?.("debug", "workflow:lifecycle", "terminateRunningRuns", { runId: run.runId, reason });
335
+ run.state.error = reason;
336
+ // A4: transition 内部先 releaseRuntime(cleanup before mutate)
337
+ run.transition("done", "failed");
338
+ await deps.store.save(run);
339
+ // C-4: run 到达 done 终态 → 注销 pending-notification(reason 固定 "failed")
340
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: "failed" });
341
+ deps.log?.("debug", "workflow:lifecycle", "run terminated", { runId: run.runId, reason: run.state.reason });
342
+ } catch (err) {
343
+ const msg = err instanceof Error ? err.message : String(err);
344
+ logger.error(
345
+ `[workflow] terminateRunningRuns failed for run ${run.runId}: ${msg} (reason: ${reason})`,
346
+ );
347
+ }
348
+ }
349
+ }
350
+
351
+ // ── evictDoneRunsBeyondCap(done run 内存淘汰,原地裁剪函数) ────────
352
+
353
+ /**
354
+ * 淘汰 runs Map 中超出保留窗口的 done run,返回本次淘汰数量。
355
+ *
356
+ * 规则(契约 W3C1):
357
+ * 1. **状态白名单**:仅 `state.status === "done"` 可淘汰(RunStatus 封闭两态,
358
+ * 显式白名单而非「非 running」——未来新增状态不落淘汰端)。running
359
+ * (活跃执行,isScriptRunning 遍历依赖)永不淘汰,即使 completedAt 缺失也
360
+ * 绝不参与排序淘汰。
361
+ * 2. **排序**:done 项按 `meta.completedAt` ISO 字符串字典序升序(toISOString 恒
362
+ * UTC 毫秒格式,字典序=时间序)。completedAt 缺失(防御旧格式/异常快照)fallback
363
+ * 排序键为空串——字典序最小=最旧,先被淘汰。
364
+ * 3. **tie 稳定排序**:比较器三态返回(相等返回 0),Array#sort 稳定性(Node≥12)
365
+ * 保持元素原序——原序 = Map 插入序 = 创建序,tie 组内先创建者视为更旧先被淘汰
366
+ * (kill-9 批量恢复同 ms completedAt 场景的确定性保证)。
367
+ * 4. **淘汰执行**:超限数 excess = doneCount - keepDone(<=0 时 no-op 返回 0),
368
+ * 对升序前 excess 项逐个 `runs.delete(runId)`。
369
+ * 5. **边界不变式**:禁止按 Map 插入序直接淘汰——嵌套 workflow 父 run 创建最早、
370
+ * 完成最晚,插入序淘汰会在其自身 onRunDone 同步裁剪中淘汰它,runAndWait 轮询
371
+ * 窗口内 get 不到 → 误返 "Run not found"。
372
+ * 6. **副作用边界**:只清内存 runs Map,不动磁盘 state 文件、不删
373
+ * workflow-state-link 指针条目、不发任何事件。
374
+ *
375
+ * @param runs per-session 的 run 注册表(原地裁剪)
376
+ * @param keepDone done run 保留数(生产传 MAX_RETAINED_DONE_RUNS)
377
+ * @returns 本次淘汰的 run 数量
378
+ */
379
+ export function evictDoneRunsBeyondCap(
380
+ runs: Map<string, WorkflowRun>,
381
+ keepDone: number,
382
+ ): number {
383
+ // 显式白名单:仅 done 参与淘汰(running 误删即功能破坏)
384
+ const done = Array.from(runs.values()).filter((r) => r.state.status === "done");
385
+ const excess = done.length - keepDone;
386
+ if (excess <= 0) return 0;
387
+ // ISO 字典序=时间序;缺失 fallback 空串(ISO 串恒以 '2' 开头非空,空串严格最小=最旧)
388
+ const keyOf = (r: WorkflowRun): string => r.meta.completedAt ?? "";
389
+ // 三态比较器 + sort 稳定性:tie 保持 Array.from 的 Map 插入序(=创建序)——
390
+ // 先插入者更旧先淘汰,禁止按插入序直接 slice 淘汰(边界不变式 5)
391
+ done.sort((a, b) => (keyOf(a) < keyOf(b) ? -1 : keyOf(a) > keyOf(b) ? 1 : 0));
392
+ for (const r of done.slice(0, excess)) {
393
+ runs.delete(r.runId);
394
+ }
395
+ return excess;
396
+ }