@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
@@ -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,8 +145,9 @@ 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
152
  * @param spec RunSpec(不可变输入,含 scriptSource/args)
141
153
  * @param deps LifecycleDeps(store/workerHost/runner/runs)
@@ -150,8 +162,8 @@ export async function runWorkflow(
150
162
  ): Promise<string> {
151
163
  // m3 E9:参数校验单一 chokepoint,钉在所有副作用前(generateRunId/log/signal
152
164
  // listener/runs.set/workerHost.start/store.save/pending:register)。校验失败时
153
- // zero side effects。coerceTypes 原地规范化 spec.args——worker 启动与 pause/resume
154
- // 重建共用同一对象(run.spec === spec),恢复路径参数一致。
165
+ // zero side effects。coerceTypes 原地规范化 spec.args——worker 启动与崩溃重建
166
+ // 共用同一对象(run.spec === spec),恢复路径参数一致。
155
167
  validateRunArgs(spec);
156
168
 
157
169
  const runId = generateRunId();
@@ -166,7 +178,7 @@ export async function runWorkflow(
166
178
  runId,
167
179
  spec,
168
180
  {
169
- status: "paused",
181
+ status: "running",
170
182
  budget: spec.budgetRef ?? new Budget({
171
183
  maxTokens: spec.budgetTokens,
172
184
  maxTimeMs: spec.budgetTimeMs,
@@ -192,9 +204,6 @@ export async function runWorkflow(
192
204
  );
193
205
  }
194
206
 
195
- // 注册到 deps.runs(makeHandlers + assignRuntime 需要 run 已在 map)
196
- deps.runs.set(runId, run);
197
-
198
207
  // 构造 handlers + runtime(worker + gate + controller)
199
208
  const handlers = makeHandlers(run, deps);
200
209
  const controller = new AbortController();
@@ -207,9 +216,14 @@ export async function runWorkflow(
207
216
  : undefined;
208
217
  const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
209
218
 
210
- // assignRuntime(paused running,原子绑定 runtime + status
219
+ // assignRuntime(注入 runtime,恢复 I1:running runtime!==undefined
211
220
  run.assignRuntime(runtime);
212
221
 
222
+ // 注册到 deps.runs(assignRuntime 之后——构造到 assignRuntime 之间 run 处于
223
+ // I1 跳过窗口(running 而 runtime undefined),后移保证窗口对外不可见;
224
+ // worker.start 抛错时 run 未注册,无孤儿 run 残留)
225
+ deps.runs.set(runId, run);
226
+
213
227
  await deps.store.save(run);
214
228
  deps.log?.("debug", "workflow:lifecycle", "run saved", { runId, status: run.state.status });
215
229
 
@@ -226,112 +240,10 @@ export async function runWorkflow(
226
240
  return runId;
227
241
  }
228
242
 
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
243
  // ── abortRun ─────────────────────────────────────────────────
332
244
 
333
245
  /**
334
- * 中止 workflow(running 或 paused)。
246
+ * 中止 workflow(running)。
335
247
  *
336
248
  * **done 状态 no-op**:已终态的 run 不重复 abort。
337
249
  * **A4 原子性**:transition("done", doneReason) 内部先 releaseRuntime。
@@ -361,13 +273,6 @@ export async function abortRun(
361
273
  return;
362
274
  }
363
275
 
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
276
  // 记录中止原因
372
277
  if (reason) {
373
278
  run.state.error = reason;
@@ -382,3 +287,101 @@ export async function abortRun(
382
287
  deps.log?.("debug", "workflow:lifecycle", "emit pending:unregister done", { runId });
383
288
  deps.onRunDone?.(run);
384
289
  }
290
+
291
+ // ── terminateRunningRuns(session 切换/关闭:终止全部 running run) ────────
292
+
293
+ /**
294
+ * 终止 deps.runs 中全部 running run(session 切换 / session 关闭时调用)。
295
+ *
296
+ * 一次性生命周期(D-2):session 离开当刻,running run 的 token 投入作废,转
297
+ * done,failed 持久化落盘——重启后 kill-9 恢复不误判,也不再存在「挂起待恢复」
298
+ * 的中间态。
299
+ *
300
+ * per-run 行为:`state.error = reason` → `transition("done","failed")`(内部先
301
+ * releaseRuntime,A4)→ `await store.save(run)` → `eventBus.emit("pending:unregister",
302
+ * {reason:"failed"})`。
303
+ *
304
+ * **不调 deps.onRunDone**:对齐 session_start 恢复先例(index.ts kill-9 恢复只发
305
+ * unregister、不发 onRunDone)——session 切换/关闭语境下主 agent 已离开本 session,
306
+ * 注入完成通知只会把消息发给已离开的 session。
307
+ *
308
+ * **不调 discardInFlightCalls**:run 已转终态不再 replay(无恢复路径),在飞 call
309
+ * 缓存清不清都不影响结果;该清理仅 rebuildRuntime 需要(崩溃重试会重放脚本,
310
+ * 假失败结果会污染重跑输出)。
311
+ *
312
+ * 单 run 失败(try/catch + log 带 runId/reason)不中断其余 run——终止是批量收尾,
313
+ * 一个 run 落盘失败不应放走其余 run 的 failed 状态。
314
+ *
315
+ * @param deps LifecycleDeps(runs/store/eventBus/log)
316
+ * @param reason 终止原因(写入 run.state.error,如 "Session switched: run terminated")
317
+ */
318
+ export async function terminateRunningRuns(
319
+ deps: LifecycleDeps,
320
+ reason: string,
321
+ ): Promise<void> {
322
+ for (const run of deps.runs.values()) {
323
+ if (run.state.status !== "running") continue;
324
+ try {
325
+ deps.log?.("debug", "workflow:lifecycle", "terminateRunningRuns", { runId: run.runId, reason });
326
+ run.state.error = reason;
327
+ // A4: transition 内部先 releaseRuntime(cleanup before mutate)
328
+ run.transition("done", "failed");
329
+ await deps.store.save(run);
330
+ // C-4: run 到达 done 终态 → 注销 pending-notification(reason 固定 "failed")
331
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: "failed" });
332
+ deps.log?.("debug", "workflow:lifecycle", "run terminated", { runId: run.runId, reason: run.state.reason });
333
+ } catch (err) {
334
+ const msg = err instanceof Error ? err.message : String(err);
335
+ logger.error(
336
+ `[workflow] terminateRunningRuns failed for run ${run.runId}: ${msg} (reason: ${reason})`,
337
+ );
338
+ }
339
+ }
340
+ }
341
+
342
+ // ── evictDoneRunsBeyondCap(done run 内存淘汰,原地裁剪函数) ────────
343
+
344
+ /**
345
+ * 淘汰 runs Map 中超出保留窗口的 done run,返回本次淘汰数量。
346
+ *
347
+ * 规则(契约 W3C1):
348
+ * 1. **状态白名单**:仅 `state.status === "done"` 可淘汰(RunStatus 封闭两态,
349
+ * 显式白名单而非「非 running」——未来新增状态不落淘汰端)。running
350
+ * (活跃执行,isScriptRunning 遍历依赖)永不淘汰,即使 completedAt 缺失也
351
+ * 绝不参与排序淘汰。
352
+ * 2. **排序**:done 项按 `meta.completedAt` ISO 字符串字典序升序(toISOString 恒
353
+ * UTC 毫秒格式,字典序=时间序)。completedAt 缺失(防御旧格式/异常快照)fallback
354
+ * 排序键为空串——字典序最小=最旧,先被淘汰。
355
+ * 3. **tie 稳定排序**:比较器三态返回(相等返回 0),Array#sort 稳定性(Node≥12)
356
+ * 保持元素原序——原序 = Map 插入序 = 创建序,tie 组内先创建者视为更旧先被淘汰
357
+ * (kill-9 批量恢复同 ms completedAt 场景的确定性保证)。
358
+ * 4. **淘汰执行**:超限数 excess = doneCount - keepDone(<=0 时 no-op 返回 0),
359
+ * 对升序前 excess 项逐个 `runs.delete(runId)`。
360
+ * 5. **边界不变式**:禁止按 Map 插入序直接淘汰——嵌套 workflow 父 run 创建最早、
361
+ * 完成最晚,插入序淘汰会在其自身 onRunDone 同步裁剪中淘汰它,runAndWait 轮询
362
+ * 窗口内 get 不到 → 误返 "Run not found"。
363
+ * 6. **副作用边界**:只清内存 runs Map,不动磁盘 state 文件、不删
364
+ * workflow-state-link 指针条目、不发任何事件。
365
+ *
366
+ * @param runs per-session 的 run 注册表(原地裁剪)
367
+ * @param keepDone done run 保留数(生产传 MAX_RETAINED_DONE_RUNS)
368
+ * @returns 本次淘汰的 run 数量
369
+ */
370
+ export function evictDoneRunsBeyondCap(
371
+ runs: Map<string, WorkflowRun>,
372
+ keepDone: number,
373
+ ): number {
374
+ // 显式白名单:仅 done 参与淘汰(running 误删即功能破坏)
375
+ const done = Array.from(runs.values()).filter((r) => r.state.status === "done");
376
+ const excess = done.length - keepDone;
377
+ if (excess <= 0) return 0;
378
+ // ISO 字典序=时间序;缺失 fallback 空串(ISO 串恒以 '2' 开头非空,空串严格最小=最旧)
379
+ const keyOf = (r: WorkflowRun): string => r.meta.completedAt ?? "";
380
+ // 三态比较器 + sort 稳定性:tie 保持 Array.from 的 Map 插入序(=创建序)——
381
+ // 先插入者更旧先淘汰,禁止按插入序直接 slice 淘汰(边界不变式 5)
382
+ done.sort((a, b) => (keyOf(a) < keyOf(b) ? -1 : keyOf(a) > keyOf(b) ? 1 : 0));
383
+ for (const r of done.slice(0, excess)) {
384
+ runs.delete(r.runId);
385
+ }
386
+ return excess;
387
+ }