@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -203,44 +203,44 @@ export async function executeAgentCall(
203
203
 
204
204
  const result = await runner.run(call.opts, signal, onEvent, stream);
205
205
 
206
- // 累加 usage(加权由 budget.consume 内部按权重常量处理,见 budget.ts)
206
+ // 累加 usage(加权由 budget.consume 内部按权重常量处理,见 budget.ts)
207
207
  if (result.usage) {
208
208
  budget.consume(result.usage);
209
209
  }
210
210
 
211
- // stale-context:不重试(P1-5)
211
+ // stale-context:不重试(P1-5)
212
212
  if (result.error !== undefined && isStaleContextErrorMsg(result.error)) {
213
213
  finalizeCall(call, result, trace, isOrphaned);
214
214
  budget.incrementCallCount();
215
215
  return;
216
216
  }
217
217
 
218
- // [MF-1] 确定性 schema 失败:不重试(gate 终止/不可满足 schema 同 schema 重试必同
219
- // 结果——重试纯烧钱;三态可重试性矩阵见 DETERMINISTIC_SCHEMA_FAILURE_PREFIX)
218
+ // [MF-1] 确定性 schema 失败:不重试(gate 终止/不可满足 schema 同 schema 重试必同
219
+ // 结果——重试纯烧钱;三态可重试性矩阵见 DETERMINISTIC_SCHEMA_FAILURE_PREFIX)
220
220
  if (result.error !== undefined && isDeterministicSchemaFailureMsg(result.error)) {
221
221
  finalizeCall(call, result, trace, isOrphaned);
222
222
  budget.incrementCallCount();
223
223
  return;
224
224
  }
225
225
 
226
- // signal 已 abort:调用方终止,不重试(避免无意义的递归)
226
+ // signal 已 abort:调用方终止,不重试(避免无意义的递归)
227
227
  if (signal.aborted) {
228
228
  finalizeCall(call, result, trace, isOrphaned);
229
229
  budget.incrementCallCount();
230
230
  return;
231
231
  }
232
232
 
233
- // 预算超限:不重试(重试只会突破预算且无意义)
233
+ // 预算超限:不重试(重试只会突破预算且无意义)
234
234
  if (result.error !== undefined && budget.isExceeded()) {
235
235
  finalizeCall(call, result, trace, isOrphaned);
236
236
  budget.incrementCallCount();
237
237
  return;
238
238
  }
239
239
 
240
- // 可重试失败:退避后递归
240
+ // 可重试失败:退避后递归
241
241
  if (result.error !== undefined && call.attempts < MAX_ATTEMPTS) {
242
242
  await delay(backoffDelay(call.attempts));
243
- // 退避期间 signal 可能 abort
243
+ // 退避期间 signal 可能 abort
244
244
  if (signal.aborted) {
245
245
  finalizeCall(call, result, trace, isOrphaned);
246
246
  budget.incrementCallCount();
@@ -250,7 +250,7 @@ export async function executeAgentCall(
250
250
  return;
251
251
  }
252
252
 
253
- // 终态(成功或达到重试上限的失败)
253
+ // 终态(成功或达到重试上限的失败)
254
254
  finalizeCall(call, result, trace, isOrphaned);
255
255
  budget.incrementCallCount();
256
256
  }
@@ -0,0 +1,327 @@
1
+ // src/orchestration/file-run-store.ts
2
+ //
3
+ // RunStore port 的通用文件实现(D2 设计件——zsw 回接 host-surface 单元)。
4
+ //
5
+ // 为什么需要它:pi 壳的 JsonlRunStore 深耦合 pi session(appendEntry /
6
+ // sessionManager,经 pi SDK 落盘 session JSONL),zcode 侧宿主没有这两个设施,
7
+ // 无法复用。RunStore port 早在 ports.ts 定义却只有 pi 一份 Infra 实现——本文件
8
+ // 补上「宿主无关」的第二份实现,双宿主的 workflow state 持久化从此同源(消灭
9
+ // 失败模式 B:行为不一致各自修)。
10
+ //
11
+ // 落盘布局:<dataRoot>/workflow-state/<runId>.jsonl(D2 规定,与 pi 壳
12
+ // <sessionDir>/workflow-state/<runId>.jsonl 同名分量、锚点不同:pi 锚 session,
13
+ // 本实现锚宿主数据根——zcode 宿主无 session dir 概念,daemon 重启后按 dataRoot
14
+ // 重水合孤儿 run)。
15
+ //
16
+ // dataRoot 通道选型:直接走 getHostServices().dataRoot()(core/host-services.ts),
17
+ // 不用 getEngineDataDir(engine/common/data-dir.ts)——后者是引擎 journal/隔离池
18
+ // 通道,带 XYZ_AGENT_DATA_DIR env 优先 + warn-once 语义(xyz-agent 宿主注入专用);
19
+ // workflow run 快照是宿主编排状态,语义归属宿主数据根本身,宿主 configureCore
20
+ // 注入什么就落什么,不引入第二条 env 覆盖链。
21
+
22
+ import { appendFile, mkdir, readdir, readFile, stat, unlink } from "node:fs/promises";
23
+ import { join } from "node:path";
24
+
25
+ import { getHostServices } from "../core/host-services.ts";
26
+ import { getLogger } from "../core/logger.ts";
27
+ import type { RunStore } from "./models/ports.ts";
28
+ import { WorkflowRun } from "./models/workflow-run.ts";
29
+ import { SNAPSHOT_VERSION, fromRunSnapshot, toRunSnapshot } from "./run-snapshot.ts";
30
+
31
+ const logger = getLogger("file-run-store");
32
+
33
+ /** run 状态目录名(<dataRoot> 下的固定分量)。 */
34
+ const STATE_DIR_NAME = "workflow-state";
35
+
36
+ // ── 磁盘保留(C1,语义对齐 pi jsonl-run-store mtime 裁剪) ─────────
37
+
38
+ /** run state 文件名 glob:runId 形如 `wf-<ts>-<rand>`(lifecycle.ts 生成),只删命中者。
39
+ * 同目录可能存在的非 state 文件永不碰(对齐 pi STATE_FILE_GLOB)。 */
40
+ const STATE_FILE_GLOB = /^wf-.*\.jsonl$/;
41
+
42
+ /**
43
+ * 磁盘保留默认上限(OR-5 跨 run 保留修复):envName 通道在 env 未设/空时生效。
44
+ *
45
+ * OR-5 将「STATE_MAX_RUNS opt-in 默认关」(无界累积)改为默认开:跨 run state
46
+ * 文件按 mtime 裁剪到本上限。取值 50 是无真实 run 体积分布数据下的保守值
47
+ * (设计 §11-4:标定待 S-A 验收后复核)——偏大不碍事(有界即达标),偏小会
48
+ * 误删仍被引用的 run 缓存,故取保守端。env 显式设置(有效正数)优先于本值;
49
+ * 显式非法值是 opt-out 通道(不清理,见 pruneStateFilesBeyondCap)。
50
+ */
51
+ export const DEFAULT_STATE_MAX_RUNS = 50;
52
+
53
+ // ── save 节流(OR-5 单 run 快照 O(n²) 主修) ──────────────────
54
+
55
+ /**
56
+ * 同一 run 两次快照落盘的最小间隔(ms)。OR-5 单 run O(n²) 主修参数:现状每
57
+ * 次 save 都 append 全量快照(快照体积 O(calls) × save 次数 O(calls)),节流后
58
+ * 落盘次数有界为 ceil(run 时长 / 本间隔)(§11-4 量级推演见 impl-plan 偏差登记:
59
+ * 100-call run 从 ~200 次落盘 / ~50MB 降到 ~17 次 / ~8MB,增量 append diff 需
60
+ * 改造两宿主共享 codec(基线+delta 行 + loadAll 重放 + 版本兼容),收益不抵
61
+ * 复杂度,节流即终案)。取值对齐 jsonl-run-store 去抖同款考量:agent-call 间隔
62
+ * 秒级,60s 窗口把快照次数压到与「分钟级 run 时长」同量级,又不让崩溃窗口
63
+ * (未落盘的 running 尾部丢失,等价崩溃链由恢复路径收编)超出分钟级。
64
+ */
65
+ export const DEFAULT_SAVE_MIN_INTERVAL_MS = 60_000;
66
+
67
+ /** FileRunStore 构造参数(全部可选;缺省即生产形态)。 */
68
+ export interface FileRunStoreOptions {
69
+ /**
70
+ * save 节流最小间隔(ms);0 = 禁用节流(每次 save 都落盘)。缺省
71
+ * {@link DEFAULT_SAVE_MIN_INTERVAL_MS}。测试经此注入小窗口(fake timers 推进)。
72
+ */
73
+ saveMinIntervalMs?: number;
74
+ }
75
+
76
+ /** Node fs 错误 code 判定(ENOENT = 路径不存在,并发删除场景;对齐 pi isEnoentError)。 */
77
+ function isEnoentError(err: unknown): boolean {
78
+ return typeof err === "object" && err !== null && "code" in err &&
79
+ (err as { code?: unknown }).code === "ENOENT";
80
+ }
81
+
82
+ // ── 快照形状 / 序列化 / 重水合 ────────────────────────────────
83
+ //
84
+ // 投影与版本衔接语义收敛于 ./run-snapshot.ts 单源 codec(下沉收口 D4/U8):
85
+ // 本 store 只保留 IO 策略(append-only + 从尾向头取最后有效行)。版本衔接的
86
+ // 宿主侧职责(D4 裁决②③,见 parseLine):「缺 v 宽容读」预处理与「版本不
87
+ // 匹配 warn 可见性」在此实现——不内聚进 codec,保 pi 侧「v1 存量静默跳过」
88
+ // 语义不被宽容化误读。
89
+
90
+ // ── FileRunStore ────────────────────────────────────────────
91
+
92
+ /**
93
+ * RunStore port 的宿主无关文件实现(port 见 models/ports.ts)。
94
+ *
95
+ * - save:append-only + 节流——快照行仍全量(崩溃时旧快照仍在,loadAll 取最后
96
+ * 一条有效行恢复到最后一致状态),但同一 running run 两次落盘有最小间隔
97
+ * (OR-5 ⑥a:节流前每次状态变更都 append 全量快照,快照体积 O(calls) ×
98
+ * save 次数 O(calls) = 单 run 磁盘 O(n²);节流参数与语义见 save 注释)。
99
+ * - loadAll:扫 <dataRoot>/workflow-state/*.jsonl,每文件从尾向头取第一条形状
100
+ * 有效的快照行;损坏行(JSON.parse 失败 / 形状校验不过 / 版本不匹配)跳过并
101
+ * warn——单行损坏不拖垮整个 run 的恢复(与 pi 壳 kill-9 恢复同容忍度)。
102
+ * 版本衔接(快照 codec 归 run-snapshot.ts 单源,D4):存量无 v 行按当前版本
103
+ * 宽容读、写入恒补 v、v 不匹配跳过 + warn(三裁决明细见 parseLine 注释)。
104
+ * - stateFilePath:纯路径计算(<dataRoot>/workflow-state/<runId>.jsonl),不建目录。
105
+ *
106
+ * 未 configureCore 即 save/loadAll 会抛 core_host_not_configured(dataRoot 端口
107
+ * 语义,host-services.ts §3.4)——宿主壳必须在初始化最早期注入。
108
+ */
109
+ export class FileRunStore implements RunStore {
110
+ /** run 状态目录绝对路径(dataRoot 每次现取——宿主覆盖配置即刻生效,对齐
111
+ * data-dir.ts「不缓存路径防测试/宿主切换读到旧值」先例)。 */
112
+ private stateDir(): string {
113
+ return join(getHostServices().dataRoot(), STATE_DIR_NAME);
114
+ }
115
+
116
+ /** save 节流最小间隔(ms),0 = 禁用。 */
117
+ private readonly saveMinIntervalMs: number;
118
+ /**
119
+ * per-runId 上次实际落盘时刻(节流判据)。终态落盘成功即删(终态后 runId 不再
120
+ * save);残留条目只出现在「running 中 run 消失(崩溃/宿主弃用)」场景,单条
121
+ * ~100B 可忽略(对齐 jsonl-run-store chains「每 runId 残留 settled Promise」
122
+ * 的取舍先例)。时间源 Date.now()(fake timers 下可推进,测试友好)。
123
+ */
124
+ private readonly lastSavedAt = new Map<string, number>();
125
+
126
+ constructor(opts?: FileRunStoreOptions) {
127
+ this.saveMinIntervalMs = Math.max(0, opts?.saveMinIntervalMs ?? DEFAULT_SAVE_MIN_INTERVAL_MS);
128
+ }
129
+
130
+ stateFilePath(runId: string): string {
131
+ return join(this.stateDir(), `${runId}.jsonl`);
132
+ }
133
+
134
+ /**
135
+ * 快照落盘(OR-5 ⑥a 节流后):
136
+ * - 首写(该 runId 尚无落盘记录)永不节流——保证新 run 至少一条快照,
137
+ * loadAll 重水合可发现;
138
+ * - 终态(status 非 running)永不节流——最终状态必落盘,末行即终态快照;
139
+ * - running 中间态距上次落盘不足 {@link saveMinIntervalMs} → 跳过本次 append
140
+ * (状态仍在调用方内存 runs Map,下次落盘带全量最新快照;本文件最后一条
141
+ * 快照因此最多落后真实状态一个节流窗口——崩溃语义与 jsonl-run-store 去抖
142
+ * 同源:未落盘的 running 尾部丢失,等价崩溃链由恢复路径收编)。
143
+ *
144
+ * 节流判据在落盘成功后才更新(IO 失败不吞下一次重试机会)。
145
+ */
146
+ async save(run: WorkflowRun): Promise<void> {
147
+ const isTerminal = run.state.status !== "running";
148
+ const now = Date.now();
149
+ const last = this.lastSavedAt.get(run.runId);
150
+ if (!isTerminal && last !== undefined && now - last < this.saveMinIntervalMs) {
151
+ return; // 节流窗口内:跳过本次全量快照 append
152
+ }
153
+ // mkdir recursive 每次 save 前执行:幂等零成本(目录已存在时仅一次 stat),
154
+ // 且免「构造时预建」——构造时建会在宿主尚未 configureCore 的窗口抛错。
155
+ await mkdir(this.stateDir(), { recursive: true });
156
+ // toRunSnapshot 补 v 字段(D4 裁决②写入侧)+ strip live 落盘
157
+ const line = JSON.stringify(toRunSnapshot(run));
158
+ await appendFile(this.stateFilePath(run.runId), line + "\n", "utf8");
159
+ if (isTerminal) {
160
+ this.lastSavedAt.delete(run.runId);
161
+ } else {
162
+ this.lastSavedAt.set(run.runId, now);
163
+ }
164
+ }
165
+
166
+ async loadAll(): Promise<WorkflowRun[]> {
167
+ let files: string[];
168
+ try {
169
+ files = await readdir(this.stateDir());
170
+ } catch {
171
+ // 目录不存在 = 从未持久化过(首启/干净环境),空集是正常态不是错误。
172
+ return [];
173
+ }
174
+
175
+ const runs: WorkflowRun[] = [];
176
+ for (const file of files) {
177
+ if (!file.endsWith(".jsonl")) continue;
178
+ const run = await this.loadLatestValidLine(join(this.stateDir(), file), file);
179
+ if (run) runs.push(run);
180
+ }
181
+ return runs;
182
+ }
183
+
184
+ /** 单文件从尾向头取第一条有效快照行;整文件无有效行返回 undefined(warn)。 */
185
+ private async loadLatestValidLine(absPath: string, display: string): Promise<WorkflowRun | undefined> {
186
+ let content: string;
187
+ try {
188
+ content = await readFile(absPath, "utf8");
189
+ } catch (err) {
190
+ const msg = err instanceof Error ? err.message : String(err);
191
+ logger.warn(`[file-run-store] skip unreadable state file ${display}: ${msg}`);
192
+ return undefined;
193
+ }
194
+
195
+ const lines = content.split("\n");
196
+ for (let i = lines.length - 1; i >= 0; i--) {
197
+ const line = lines[i].trim();
198
+ if (line === "") continue; // 尾部空行(末行 \n 产物)静默跳过
199
+ const run = this.parseLine(line, display, i);
200
+ if (run) return run;
201
+ // 损坏行 warn 后继续向前找——最后一条「有效」行可能早于文件尾部(半行写入崩溃)
202
+ }
203
+ logger.warn(`[file-run-store] no valid snapshot line in ${display} (empty or all corrupted)`);
204
+ return undefined;
205
+ }
206
+
207
+ /**
208
+ * 单行解析 + 版本衔接预处理(D4 裁决②③,宿主侧职责)+ 形状校验;损坏
209
+ * warn 并返回 undefined。
210
+ *
211
+ * - 缺 v 字段(core 存量行)→ 就地补当前版本再进 codec(「缺版本 = 当前
212
+ * 版本」宽容读,不做自动迁移——写回时经 toRunSnapshot 自然补 v 完成渐进
213
+ * 收敛);预处理留在 store 层而非 codec,保 pi 侧「v1 存量静默跳过」语义
214
+ * 不被宽容化误读(D4 裁决②归属裁决)。
215
+ * - v 存在但不匹配(未知更高版本/降级写入)→ 跳过 + warn(补可见性,对齐
216
+ * pi 静默跳过语义;字符串版本无大小序,不引入比较逻辑——D4 裁决③)。
217
+ * 此处版本判断仅为 warn 可见性,数据防线仍是 codec 内 guard(双保险,
218
+ * pi 切换 codec 后共享同一防线)。
219
+ */
220
+ private parseLine(line: string, display: string, lineNo: number): WorkflowRun | undefined {
221
+ let parsed: unknown;
222
+ try {
223
+ parsed = JSON.parse(line);
224
+ } catch (err) {
225
+ const msg = err instanceof Error ? err.message : String(err);
226
+ logger.warn(`[file-run-store] skip corrupted line ${display}:${lineNo}: ${msg}`);
227
+ return undefined;
228
+ }
229
+ if (parsed !== null && typeof parsed === "object") {
230
+ const rec = parsed as { v?: unknown };
231
+ if (rec.v === undefined) {
232
+ rec.v = SNAPSHOT_VERSION;
233
+ } else if (rec.v !== SNAPSHOT_VERSION) {
234
+ logger.warn(
235
+ `[file-run-store] skip snapshot with unsupported version ${display}:${lineNo}: v=${JSON.stringify(rec.v)} (this build only reads v=${JSON.stringify(SNAPSHOT_VERSION)}; the run line is skipped). To recover: upgrade @zhushanwen/subagent-core, or migrate/delete this state file if its runs are no longer needed`,
236
+ );
237
+ return undefined;
238
+ }
239
+ }
240
+ const run = fromRunSnapshot(parsed);
241
+ if (run === undefined) {
242
+ logger.warn(`[file-run-store] skip malformed snapshot ${display}:${lineNo} (shape validation failed)`);
243
+ return undefined;
244
+ }
245
+ return run;
246
+ }
247
+
248
+ /**
249
+ * 把 workflow-state 目录裁剪到上限个最新 state 文件(mtime 升序删最旧,C1)。
250
+ *
251
+ * 语义对齐 pi jsonl-run-store.pruneStateFilesBeyondCap(逐段同构):
252
+ * - 只删本目录内命中 {@link STATE_FILE_GLOB} 的文件;任何失败都不抛(清理是
253
+ * 旁路维护,不能拖垮持久化主链路):readdir 失败静默放弃本轮(ENOENT =
254
+ * 从未持久化,正常态),单个 unlink 失败(非 ENOENT)warn 留证后继续删
255
+ * 其余——ENOENT 视为并发删除竞态下的已达成目标,不告警;
256
+ * - stat 全集取 mtime,allSettled 部分降级——单文件 stat 失败(并发删除
257
+ * ENOENT 等)静默跳过该文件,不阻断本轮裁剪。
258
+ *
259
+ * 上限解析(envName 通道,OR-5 ⑥b 默认开;显式非法值 opt-out 对齐 pi 解析风格):
260
+ * - `envName` 提供 → env 通道:`process.env[envName]` 未设/空 → 按默认上限
261
+ * {@link DEFAULT_STATE_MAX_RUNS} 裁剪(**默认开**——OR-5 修复前的 opt-in
262
+ * 「默认关」正是跨 run 无界累积缺陷本身);设了有限正数 → 上限 = env 值
263
+ * (env 值即上限);设了非法值(非有限数/≤0)→ 不清理(显式 opt-out 通道:
264
+ * 用户意图不明时不动磁盘——对齐本方法 readdir/stat 失败一律放弃的保守哲学,
265
+ * 宿主如需自管保留可设足够大的正数值);
266
+ * - `envName` 缺省 → 无 env 通道,直接按 `max` 参数裁剪(上限 = max,调用方
267
+ * 自管启用时机)。
268
+ *
269
+ * 本方法只做磁盘裁剪,不动内存 runs Map(内存侧淘汰归
270
+ * lifecycle.evictDoneRunsBeyondCap,两域独立)。
271
+ *
272
+ * @param max 上限(envName 缺省时生效;env 通道启用时被 env 值覆盖)
273
+ * @param envName opt-in 开关 + 上限覆盖 env 变量名(可选;pi 先例
274
+ * `XYZ_SUBAGENT_STATE_MAX_RUNS`)
275
+ */
276
+ async pruneStateFilesBeyondCap(max: number, envName?: string): Promise<void> {
277
+ let cap = max;
278
+ if (envName !== undefined) {
279
+ // 未设/空 → 默认开(OR-5 ⑥b:DEFAULT_STATE_MAX_RUNS);非法/≤0 → 不清理
280
+ // (显式 opt-out 通道,见方法注释);有效正数 → env 值覆盖
281
+ const raw = process.env[envName];
282
+ if (raw === undefined || raw === "") {
283
+ cap = DEFAULT_STATE_MAX_RUNS;
284
+ } else {
285
+ const parsed = Number(raw);
286
+ if (!Number.isFinite(parsed) || parsed <= 0) return;
287
+ cap = parsed;
288
+ }
289
+ }
290
+
291
+ const stateDir = this.stateDir();
292
+ let names: string[];
293
+ try {
294
+ names = await readdir(stateDir);
295
+ } catch (err) {
296
+ if (!isEnoentError(err)) {
297
+ const reason = err instanceof Error ? err.message : String(err);
298
+ logger.warn(`[file-run-store] state retention: readdir ${stateDir} failed: ${reason}`);
299
+ }
300
+ return;
301
+ }
302
+ const stateFiles = names.filter((n) => STATE_FILE_GLOB.test(n)).sort();
303
+ if (stateFiles.length <= cap) return;
304
+
305
+ // stat 全集取 mtime;allSettled 部分降级(单文件失败静默跳过,不阻断本轮)
306
+ const settled = await Promise.allSettled(
307
+ stateFiles.map(async (name) => {
308
+ const full = join(stateDir, name);
309
+ return { full, mtimeMs: (await stat(full)).mtimeMs };
310
+ }),
311
+ );
312
+ const byMtimeAsc = settled
313
+ .flatMap((r) => (r.status === "fulfilled" ? [r.value] : []))
314
+ .sort((a, b) => a.mtimeMs - b.mtimeMs);
315
+ const victims = byMtimeAsc.slice(0, byMtimeAsc.length - cap);
316
+ for (const victim of victims) {
317
+ try {
318
+ await unlink(victim.full);
319
+ logger.debug(`[file-run-store] state retention: pruned ${victim.full}`);
320
+ } catch (err) {
321
+ if (isEnoentError(err)) continue; // 并发删除已达成目标
322
+ const reason = err instanceof Error ? err.message : String(err);
323
+ logger.warn(`[file-run-store] state retention: failed to delete ${victim.full}: ${reason}`);
324
+ }
325
+ }
326
+ }
327
+ }
@@ -184,11 +184,11 @@ async function pollRunToResult(
184
184
  return finalRun
185
185
  ? toResult(finalRun)
186
186
  : {
187
- status: "done",
188
- reason: "time_limited",
189
- error: `Workflow timed out after ${explicitTimeoutMs}ms`,
190
- runId,
191
- };
187
+ status: "done",
188
+ reason: "time_limited",
189
+ error: `Workflow timed out after ${explicitTimeoutMs}ms`,
190
+ runId,
191
+ };
192
192
  }
193
193
 
194
194
  // ── runAndWait ───────────────────────────────────────────────
@@ -214,6 +214,11 @@ async function pollRunToResult(
214
214
  * @param timeoutMs 超时上限(可选)。[预算语义对齐 + U2] 未传或 <=0 = 不限(轮询至 done /
215
215
  * abort 为止,不限时由 XYZ_SUBAGENT_RUN_WATCHDOG_MS 兜底)——旧实现默认 10 分钟会误杀长任务,
216
216
  * 且 0/负值会落成立即超时;仅显式正数才限时。
217
+ * @param model Run 级 model override(可选)。[host-surface] 经 spec.model → workerData →
218
+ * $MODEL → agent() fallback(RunSpec Option B 同一路径)。缺省不覆盖——宿主未显式指定
219
+ * model 时维持脚本内 agent() 显式参数 / agent .md / 引擎默认的既有解析序。此前该入口
220
+ * 无 model 通道,消费方(zsw CLI --model)只能丢弃该参数——行为劈叉于走 buildSpec 的
221
+ * 异步入口。
217
222
  * @returns WorkflowRunResult(status 恒 "done")
218
223
  */
219
224
  export async function runAndWait(
@@ -222,8 +227,9 @@ export async function runAndWait(
222
227
  deps: LauncherDeps,
223
228
  signal?: AbortSignal,
224
229
  timeoutMs?: number,
230
+ model?: string,
225
231
  ): Promise<WorkflowRunResult> {
226
- // 1. registry 查找脚本(workflowRef = 绝对路径,S2 路径统一)
232
+ // 1. registry 查找脚本(workflowRef = 绝对路径,S2 路径统一)
227
233
  const script = await deps.registry.getPath(name);
228
234
  if (!script) {
229
235
  return {
@@ -234,7 +240,7 @@ export async function runAndWait(
234
240
  };
235
241
  }
236
242
 
237
- // 2. lint 校验(失败抛错——脚本本身有问题,不应静默吞)
243
+ // 2. lint 校验(失败抛错——脚本本身有问题,不应静默吞)
238
244
  const lintResult = script.validate();
239
245
  if (!lintResult.valid) {
240
246
  const errors = lintResult.findings
@@ -244,26 +250,29 @@ export async function runAndWait(
244
250
  throw new Error(`Workflow script '${name}' has lint errors: ${errors}`);
245
251
  }
246
252
 
247
- // 3. 构建 RunSpec
248
- // 不设 budgetTimeMs:runAndWait 自身用轮询 deadline(pollRunToResult 内 while + safeAbort)
249
- // 实施 timeout,并产出「Workflow timed out after Xms」的具体错误信息。spec 级
250
- // 时间预算(lifecycle.scheduleTimeBudget)服务于 fire-and-forget 的交互式 run
251
- // (tool-workflow actionRun),若在此也设会与轮询 deadline 同时触发产生竞态。
253
+ // 3. 构建 RunSpec
254
+ // 不设 budgetTimeMs:runAndWait 自身用轮询 deadline(pollRunToResult 内 while + safeAbort)
255
+ // 实施 timeout,并产出「Workflow timed out after Xms」的具体错误信息。spec 级
256
+ // 时间预算(lifecycle.scheduleTimeBudget)服务于 fire-and-forget 的交互式 run
257
+ // (tool-workflow actionRun),若在此也设会与轮询 deadline 同时触发产生竞态。
258
+ // model 是例外:时间预算必须二选一(竞态),model 只有单通道(spec Option B),
259
+ // 调用方显式传入即透传,不与任何轮询面竞争。
252
260
  const spec: RunSpec = {
253
261
  scriptSource: script.toExecutable(),
254
262
  args,
255
263
  budgetTokens: undefined,
264
+ model,
256
265
  scriptName: script.name,
257
266
  scriptPath: script.path,
258
267
  description: script.meta.description,
259
268
  parameters: script.meta.parameters,
260
269
  };
261
270
 
262
- // 4. 启动 workflow + 5. 轮询至 done(含 6. timeout → abortRun,C.7)
263
- // pending-notification 的 register/unregister 由 runWorkflow(启动注册)+
264
- // transition("done") 路径(完成注销)统一处理,runAndWait 不再重复 emit。
265
- // m3:chokepoint 校验失败(ArgsValidationError)→ 返回 invalid_args 结果(run 从未
266
- // 创建,runId 恒 ''),非 ArgsValidationError 保持传播。
271
+ // 4. 启动 workflow + 5. 轮询至 done(含 6. timeout → abortRun,C.7)
272
+ // pending-notification 的 register/unregister 由 runWorkflow(启动注册)+
273
+ // transition("done") 路径(完成注销)统一处理,runAndWait 不再重复 emit。
274
+ // m3:chokepoint 校验失败(ArgsValidationError)→ 返回 invalid_args 结果(run 从未
275
+ // 创建,runId 恒 ''),非 ArgsValidationError 保持传播。
267
276
  let runId: string;
268
277
  try {
269
278
  runId = await runWorkflow(spec, deps, signal);
@@ -296,7 +305,7 @@ async function safeAbort(
296
305
  try {
297
306
  await abortRun(runId, deps, reason, doneReason);
298
307
  } catch (err) {
299
- // run 可能已终态或不存在——忽略,调用方据 toResult 判断
308
+ // run 可能已终态或不存在——忽略,调用方据 toResult 判断
300
309
  void err;
301
310
  }
302
311
  }
@@ -332,7 +341,7 @@ export async function executeNestedWorkflow(
332
341
  parentRun: WorkflowRun,
333
342
  deps: LauncherDeps,
334
343
  ): Promise<{ content: string; parsedOutput?: unknown; error?: string }> {
335
- // Step 1: 循环检测——parentWorkflowChain 不存在时为 [](顶层 run)
344
+ // Step 1: 循环检测——parentWorkflowChain 不存在时为 [](顶层 run)
336
345
  const chain = [
337
346
  ...(parentRun.spec.parentWorkflowChain ?? []),
338
347
  parentRun.spec.scriptName,
@@ -344,9 +353,9 @@ export async function executeNestedWorkflow(
344
353
  };
345
354
  }
346
355
 
347
- // Step 2: signal 继承——子 run 响应父 run abort
348
- // [L-2] 提取命名 onParentAbort 以便 finally removeEventListener,防子 run 完成后
349
- // parentSignal 上残留 listener(多次嵌套调用会累积)。
356
+ // Step 2: signal 继承——子 run 响应父 run abort
357
+ // [L-2] 提取命名 onParentAbort 以便 finally removeEventListener,防子 run 完成后
358
+ // parentSignal 上残留 listener(多次嵌套调用会累积)。
350
359
  const childController = new AbortController();
351
360
  const parentSignal = parentRun.runtime?.controller.signal;
352
361
  const onParentAbort = (): void => childController.abort();
@@ -358,10 +367,10 @@ export async function executeNestedWorkflow(
358
367
  }
359
368
  }
360
369
 
361
- // Step 3+:registry 查找 + lint + RunSpec + runWorkflow + poll 全程 try(m3 E8——
362
- // try 起点提到 Step 2 的 listener 注册之后,覆盖 Step 3-6。runWorkflow throw
363
- // (含 chokepoint ArgsValidationError)与 not found/lint 早返回均走 finally 移除
364
- // parentSignal listener——修复原 try 外 runWorkflow 的泄漏路径)。
370
+ // Step 3+:registry 查找 + lint + RunSpec + runWorkflow + poll 全程 try(m3 E8——
371
+ // try 起点提到 Step 2 的 listener 注册之后,覆盖 Step 3-6。runWorkflow throw
372
+ // (含 chokepoint ArgsValidationError)与 not found/lint 早返回均走 finally 移除
373
+ // parentSignal listener——修复原 try 外 runWorkflow 的泄漏路径)。
365
374
  try {
366
375
  // Step 3: registry 查找 + lint(失败返回 error result,不抛错)
367
376
  const script = await deps.registry.getPath(name);