@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
@@ -0,0 +1,382 @@
1
+ /**
2
+ * [OR-4] 终态收尾 emit/onRunDone M12 同款围栏 + [OR-6] handleWorkerMessage log case /
3
+ * default 留痕 + [OR-8] run done 时 in-flight call 收口 cancelled 测试。
4
+ *
5
+ * 覆盖(unbounded-wait-audit §4.1 OR-4 / §4.3 OR-6、OR-8 / §7.2 T3④⑤、T7④):
6
+ * - OR-4:eventBus.emit 或 onRunDone 同步抛错时,五个终态路径(return / worker error
7
+ * 超限 / script error 超限 / exit 无终态消息 / time_limited)promise 均 resolve——
8
+ * 错误被围栏捕获 error 留痕,不再经 worker-host 的 `void handlers.onXxx(...)`
9
+ * 变 unhandledRejection 崩宿主
10
+ * - OR-6 主线程半边:{type:"log"} 计入 errorLogs + deps.log debug 留痕(含上限裁剪);
11
+ * 未知 type warn 留痕(协议漂移防线)
12
+ * - OR-8:脚本 return 时残留 fire-and-forget in-flight call 收口为取消终态
13
+ * (call done + trace failed + Cancelled 文案 + completedAt + live 清除),
14
+ * 先收口再落盘——done 快照不含 running 节点
15
+ */
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
+
18
+ import { getLogger } from "../../core/logger.ts";
19
+ import {
20
+ closeOutInFlightCalls,
21
+ handleScriptError,
22
+ handleWorkerError,
23
+ handleWorkerExit,
24
+ handleWorkerMessage,
25
+ } from "../error-recovery.ts";
26
+ import { Budget } from "../models/budget.ts";
27
+ import { RunRuntime } from "../models/run-runtime.ts";
28
+ import { toRunSnapshot } from "../run-snapshot.ts";
29
+ import { Trace } from "../models/trace.ts";
30
+ import type { AgentResult, WorkerLogEntry } from "../models/types.ts";
31
+ import { WorkflowRun } from "../models/workflow-run.ts";
32
+ import type { LifecycleDeps, WorkerHandlers } from "../models/ports.ts";
33
+ import type { WorkerHandle } from "../worker-handle.ts";
34
+ import { flushMicrotasks } from "./helpers/flush-microtasks.ts";
35
+
36
+ // ── helpers ──────────────────────────────────────────────────
37
+
38
+ const MAX_ERROR_LOGS = 500;
39
+
40
+ function makeRealRun(runId: string, opts: { budgetTimeMs?: number } = {}): WorkflowRun {
41
+ const run = new WorkflowRun(
42
+ runId,
43
+ {
44
+ scriptName: "test-wf",
45
+ scriptSource: "agent('hi')",
46
+ args: {},
47
+ scriptPath: "/tmp/test-wf.js",
48
+ budgetTimeMs: opts.budgetTimeMs,
49
+ },
50
+ {
51
+ status: "running",
52
+ budget: new Budget(),
53
+ calls: new Map(),
54
+ trace: new Trace(),
55
+ errorLogs: [],
56
+ },
57
+ { startedAt: new Date().toISOString() },
58
+ );
59
+ const initialWorker = {
60
+ postMessage: vi.fn(),
61
+ terminate: vi.fn(async () => {}),
62
+ } as unknown as WorkerHandle;
63
+ run.assignRuntime(new RunRuntime(initialWorker, new AbortController()));
64
+ return run;
65
+ }
66
+
67
+ function makeDeps(opts: {
68
+ /** eventBus.emit 同步抛错(模拟 listener 异常)。 */
69
+ emitThrows?: boolean;
70
+ /** onRunDone 同步抛错(模拟 evictDoneRunsBeyondCap 等收尾异常)。 */
71
+ onRunDoneThrows?: boolean;
72
+ } = {}): LifecycleDeps & {
73
+ store: { save: ReturnType<typeof vi.fn> };
74
+ workerHost: { start: ReturnType<typeof vi.fn> };
75
+ eventBus: { emit: ReturnType<typeof vi.fn> };
76
+ onRunDone: ReturnType<typeof vi.fn>;
77
+ log: ReturnType<typeof vi.fn>;
78
+ } {
79
+ return {
80
+ store: { save: vi.fn(async () => {}) },
81
+ workerHost: {
82
+ start: vi.fn(
83
+ () => ({ postMessage: vi.fn(), terminate: vi.fn(async () => {}) }) as unknown as WorkerHandle,
84
+ ),
85
+ },
86
+ runner: { run: vi.fn(async () => ({}) as AgentResult) },
87
+ runs: new Map(),
88
+ eventBus: {
89
+ emit: opts.emitThrows
90
+ ? vi.fn(() => {
91
+ throw new Error("listener exploded");
92
+ })
93
+ : vi.fn(),
94
+ },
95
+ onRunDone: opts.onRunDoneThrows
96
+ ? vi.fn(() => {
97
+ throw new Error("onRunDone exploded");
98
+ })
99
+ : vi.fn(),
100
+ log: vi.fn(),
101
+ } as unknown as ReturnType<typeof makeDeps>;
102
+ }
103
+
104
+ function makeHandlers(): WorkerHandlers {
105
+ return {
106
+ onMessage: vi.fn(async () => {}),
107
+ onError: vi.fn(async () => {}),
108
+ onExit: vi.fn(async () => {}),
109
+ } as unknown as WorkerHandlers;
110
+ }
111
+
112
+ function makeAgentCallMsg(callId: number): unknown {
113
+ return {
114
+ type: "agent-call",
115
+ callId,
116
+ opts: { prompt: "test task", agent: "worker", description: "test-slug" },
117
+ };
118
+ }
119
+
120
+ beforeEach(() => {
121
+ vi.useFakeTimers();
122
+ });
123
+
124
+ afterEach(() => {
125
+ vi.useRealTimers();
126
+ vi.restoreAllMocks();
127
+ });
128
+
129
+ // ── [OR-4] 终态收尾围栏 ──────────────────────────────────────
130
+
131
+ describe("[OR-4] 终态收尾 emit/onRunDone 围栏(不产 unhandledRejection)", () => {
132
+ it("handleReturn:eventBus.emit 同步抛错 → promise resolve + error 留痕(save 已发生)", async () => {
133
+ const run = makeRealRun("wf-fence-1");
134
+ const deps = makeDeps({ emitThrows: true });
135
+
136
+ await expect(
137
+ handleWorkerMessage(run, { type: "return", result: { ok: true } }, deps, makeHandlers()),
138
+ ).resolves.toBeUndefined();
139
+
140
+ expect(run.state.status).toBe("done");
141
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
142
+ // [B-4] 独立围栏:emit 故障不再跳过 onRunDone(旧实现同一 try 会跳过)
143
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
144
+ });
145
+
146
+ it("handleReturn:onRunDone 同步抛错 → promise resolve + error 留痕", async () => {
147
+ const run = makeRealRun("wf-fence-2");
148
+ const deps = makeDeps({ onRunDoneThrows: true });
149
+
150
+ await expect(
151
+ handleWorkerMessage(run, { type: "return", result: { ok: true } }, deps, makeHandlers()),
152
+ ).resolves.toBeUndefined();
153
+
154
+ expect(run.state.status).toBe("done");
155
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
156
+ expect(deps.eventBus.emit).toHaveBeenCalledTimes(1); // emit 正常发出后才抛
157
+ });
158
+
159
+ it("handleWorkerError 超限:emit 抛错 → resolve(旧实现裸调 → unhandledRejection)", async () => {
160
+ const run = makeRealRun("wf-fence-3");
161
+ (run.meta as { workerErrorCount?: number }).workerErrorCount = 3; // count=4 > MAX
162
+ const deps = makeDeps({ emitThrows: true });
163
+
164
+ await expect(handleWorkerError(run, new Error("boom"), deps, makeHandlers())).resolves.toBeUndefined();
165
+ expect(run.state.reason).toBe("failed");
166
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
167
+ });
168
+
169
+ it("handleScriptError 超限:onRunDone 抛错 → resolve", async () => {
170
+ const run = makeRealRun("wf-fence-4");
171
+ (run.meta as { scriptErrorCount?: number }).scriptErrorCount = 3;
172
+ const deps = makeDeps({ onRunDoneThrows: true });
173
+
174
+ await expect(handleScriptError(run, "boom", [], deps, makeHandlers())).resolves.toBeUndefined();
175
+ expect(run.state.reason).toBe("failed");
176
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
177
+ });
178
+
179
+ it("handleWorkerExit 无终态消息路径:emit 抛错 → resolve", async () => {
180
+ const run = makeRealRun("wf-fence-5");
181
+ const deps = makeDeps({ emitThrows: true });
182
+ const handle = { isCurrent: true } as unknown as WorkerHandle;
183
+
184
+ await expect(handleWorkerExit(run, 0, handle, deps, makeHandlers())).resolves.toBeUndefined();
185
+ expect(run.state.reason).toBe("failed");
186
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it("time_limited 路径(预算耗尽 + 超限重试):emit 抛错 → resolve", async () => {
190
+ const run = makeRealRun("wf-fence-6", { budgetTimeMs: 5000 });
191
+ // 已耗 6000ms > 预算 5000ms(fake timers 冻结 Date)
192
+ run.meta.startedAt = new Date(Date.now() - 6000).toISOString();
193
+ const deps = makeDeps({ emitThrows: true });
194
+
195
+ const p = handleScriptError(run, "boom", [], deps, makeHandlers());
196
+ await vi.advanceTimersByTimeAsync(1000); // 退避
197
+ await expect(p).resolves.toBeUndefined();
198
+
199
+ expect(run.state.reason).toBe("time_limited");
200
+ expect(deps.store.save).toHaveBeenCalledTimes(1);
201
+ // [B-4] 独立围栏:emit 故障不再跳过 onRunDone(旧实现同一 try 会跳过)
202
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ it("围栏捕获后 error 留痕(shared logger)", async () => {
206
+ const errorSpy = vi.spyOn(getLogger("subagents"), "error");
207
+ const run = makeRealRun("wf-fence-7");
208
+ const deps = makeDeps({ onRunDoneThrows: true });
209
+
210
+ await handleWorkerMessage(run, { type: "return", result: 1 }, deps, makeHandlers());
211
+
212
+ const errLogs = errorSpy.mock.calls.map((c) => String(c[0]));
213
+ expect(errLogs.some((m) => m.includes("onRunDone failed") && m.includes("onRunDone exploded"))).toBe(true);
214
+ });
215
+
216
+ it("[B-4] emit 抛错 → onRunDone 仍执行(独立围栏,完成回调不被通知总线故障吞掉)", async () => {
217
+ const errorSpy = vi.spyOn(getLogger("subagents"), "error");
218
+ const run = makeRealRun("wf-fence-8");
219
+ const deps = makeDeps({ emitThrows: true });
220
+
221
+ await handleWorkerMessage(run, { type: "return", result: 1 }, deps, makeHandlers());
222
+
223
+ expect(run.state.reason).toBe("completed");
224
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1); // 旧实现同一 try:emit 抛错会跳过
225
+ const errLogs = errorSpy.mock.calls.map((c) => String(c[0]));
226
+ expect(errLogs.some((m) => m.includes("pending:unregister emit failed") && m.includes("listener exploded"))).toBe(true);
227
+ });
228
+ });
229
+
230
+ // ── [OR-6] handleWorkerMessage log case + default 留痕 ───────
231
+
232
+ describe("[OR-6] log 消息消费 + 未知类型 default 留痕", () => {
233
+ it("{type:\"log\"} 计入 run.state.errorLogs + deps.log debug 留痕(不触发终态)", async () => {
234
+ const run = makeRealRun("wf-log-1");
235
+ const deps = makeDeps();
236
+
237
+ await handleWorkerMessage(run, { type: "log", phase: "build", message: "step 1 done" }, deps, makeHandlers());
238
+
239
+ expect(run.state.errorLogs).toEqual([{ level: "log", message: "step 1 done" }]);
240
+ expect(run.state.status).toBe("running"); // 不触发终态
241
+ expect(deps.store.save).not.toHaveBeenCalled();
242
+ expect(deps.log).toHaveBeenCalledWith("debug", "workflow:error-recovery", "worker log", {
243
+ runId: "wf-log-1",
244
+ phase: "build",
245
+ message: "step 1 done",
246
+ });
247
+ });
248
+
249
+ it("log 超上限(MAX_ERROR_LOGS)时裁剪保留尾部(与 workerLogs 通路同语义)", async () => {
250
+ const run = makeRealRun("wf-log-2");
251
+ const deps = makeDeps();
252
+ const prefill: WorkerLogEntry[] = Array.from({ length: MAX_ERROR_LOGS }, (_, i) => ({
253
+ level: "log",
254
+ message: `pre-${i}`,
255
+ }));
256
+ run.state.errorLogs.push(...prefill);
257
+
258
+ await handleWorkerMessage(run, { type: "log", message: "the-latest" }, deps, makeHandlers());
259
+
260
+ expect(run.state.errorLogs).toHaveLength(MAX_ERROR_LOGS);
261
+ expect(run.state.errorLogs.at(-1)).toEqual({ level: "log", message: "the-latest" });
262
+ expect(run.state.errorLogs[0]).toEqual({ level: "log", message: "pre-1" }); // 头部被裁
263
+ });
264
+
265
+ it("[B-3] log 单通路:log 消息即时入账 + return workerLogs(仅 console.* 条目)→ 同一日志恰一份", async () => {
266
+ // worker 侧 [B-3] 后的产出形态(worker-script-builder-runtime.test.ts 锁定):
267
+ // log() 只发独立 {type:"log"} 消息,return 的 workerLogs 只含 console.* 捕获条目。
268
+ // 主线程两条消息各走各的入账 → 同一日志在 errorLogs 恰一份(旧双通路为 2 份)。
269
+ const run = makeRealRun("wf-log-dup");
270
+ const deps = makeDeps();
271
+
272
+ await handleWorkerMessage(run, { type: "log", phase: "build", message: "step-1" }, deps, makeHandlers());
273
+ await handleWorkerMessage(
274
+ run,
275
+ { type: "return", result: { ok: true }, workerLogs: [{ level: "error", message: "console-entry" }] },
276
+ deps,
277
+ makeHandlers(),
278
+ );
279
+
280
+ expect(run.state.errorLogs.filter((l) => l.message === "step-1")).toHaveLength(1);
281
+ expect(run.state.errorLogs.filter((l) => l.message === "console-entry")).toHaveLength(1);
282
+ });
283
+
284
+ it("未知消息类型:warn 留痕后丢弃(协议漂移防线),不写状态不终态", async () => {
285
+ const warnSpy = vi.spyOn(getLogger("subagents"), "warn");
286
+ const run = makeRealRun("wf-log-3");
287
+ const deps = makeDeps();
288
+
289
+ await handleWorkerMessage(run, { type: "future-unknown-type", payload: 1 }, deps, makeHandlers());
290
+
291
+ expect(warnSpy.mock.calls.map((c) => String(c[0])).some((m) => m.includes("unknown worker message type") && m.includes("future-unknown-type"))).toBe(true);
292
+ expect(deps.log).toHaveBeenCalledWith("warn", "workflow:error-recovery", "unknown worker message type", {
293
+ runId: "wf-log-3",
294
+ type: "future-unknown-type",
295
+ });
296
+ expect(run.state.status).toBe("running");
297
+ expect(deps.store.save).not.toHaveBeenCalled();
298
+ });
299
+ });
300
+
301
+ // ── [OR-8] run done 时 in-flight call 收口 cancelled ─────────
302
+
303
+ describe("[OR-8] run done 时残留 in-flight call 收口 cancelled", () => {
304
+ it("fire-and-forget agent() 后 return:call 收口 done + trace failed(Cancelled 文案),快照无 running 节点", async () => {
305
+ const run = makeRealRun("wf-or8-1");
306
+ const deps = makeDeps();
307
+ // fire-and-forget dispatch:runner 永不 settle(脚本不 await,模拟在飞子进程)
308
+ deps.runner.run.mockImplementation(() => new Promise<AgentResult>(() => {}));
309
+ await handleWorkerMessage(run, makeAgentCallMsg(1), deps, makeHandlers());
310
+ await flushMicrotasks();
311
+ expect(run.state.calls.get(1)?.status).toBe("running");
312
+
313
+ // 脚本 return(fire-and-forget call 仍在飞)
314
+ await handleWorkerMessage(run, { type: "return", result: "done early" }, deps, makeHandlers());
315
+
316
+ expect(run.state.status).toBe("done");
317
+ const call = run.state.calls.get(1);
318
+ // call 收口为 done + 取消文案(不删除条目——保留调用痕迹)
319
+ expect(call?.status).toBe("done");
320
+ expect(call?.result?.error).toContain("Cancelled");
321
+ // trace 节点 failed + completedAt + live 清除(快照/GUI 不再显示 running 步骤)
322
+ const node = run.state.trace.find(1);
323
+ expect(node?.status).toBe("failed");
324
+ expect(node?.error).toContain("Cancelled");
325
+ expect(node?.completedAt).toBeDefined();
326
+ expect(node?.live).toBeUndefined();
327
+ // 持久化快照(收口先于 save)不含 running 形态
328
+ const snap = toRunSnapshot(run);
329
+ expect(snap.state.calls.map((c) => c.status)).toEqual(["done"]);
330
+ expect(snap.state.trace.map((n) => n.status)).toEqual(["failed"]);
331
+ });
332
+
333
+ it("handleWorkerError 超限路径同样收口(failed 快照一致)", async () => {
334
+ const run = makeRealRun("wf-or8-2");
335
+ const deps = makeDeps();
336
+ deps.runner.run.mockImplementation(() => new Promise<AgentResult>(() => {}));
337
+ await handleWorkerMessage(run, makeAgentCallMsg(2), deps, makeHandlers());
338
+ await flushMicrotasks();
339
+
340
+ // 预置计数已达上限:count=4 > MAX → 无退避直接 failed(in-flight call 一并收口)
341
+ (run.meta as { workerErrorCount?: number }).workerErrorCount = 3;
342
+ await handleWorkerError(run, new Error("final boom"), deps, makeHandlers());
343
+
344
+ expect(run.state.reason).toBe("failed");
345
+ expect(run.state.calls.get(2)?.status).toBe("done");
346
+ expect(run.state.calls.get(2)?.result?.error).toContain("Cancelled");
347
+ expect(run.state.trace.find(2)?.status).toBe("failed");
348
+ });
349
+
350
+ it("closeOutInFlightCalls 对 pending 状态 call 补齐状态机(markRunning→markDone)", () => {
351
+ const run = makeRealRun("wf-or8-3");
352
+ const deps = makeDeps();
353
+ deps.runner.run.mockImplementation(() => new Promise<AgentResult>(() => {}));
354
+ void handleWorkerMessage(run, makeAgentCallMsg(3), deps, makeHandlers());
355
+ // 直接构造 pending 形态(防御 pending 分支可达性)
356
+ const call = run.state.calls.get(3)!;
357
+ (call as { status: string }).status = "pending";
358
+
359
+ const closed = closeOutInFlightCalls(run);
360
+
361
+ expect(closed).toEqual([3]);
362
+ expect(call.status).toBe("done");
363
+ expect(call.result?.error).toContain("Cancelled");
364
+ });
365
+
366
+ it("无 in-flight call 时收口为 no-op(正常完成的 call 不受影响)", async () => {
367
+ const run = makeRealRun("wf-or8-4");
368
+ const deps = makeDeps();
369
+ deps.runner.run.mockImplementation(
370
+ async () => ({ content: "ok", durationMs: 1, error: undefined, toolCalls: [] }) as AgentResult,
371
+ );
372
+ await handleWorkerMessage(run, makeAgentCallMsg(4), deps, makeHandlers());
373
+ await flushMicrotasks(20);
374
+ expect(run.state.calls.get(4)?.status).toBe("done");
375
+
376
+ await handleWorkerMessage(run, { type: "return", result: "ok" }, deps, makeHandlers());
377
+
378
+ // 已完成 call 的结果原样保留(不被取消文案覆盖)
379
+ expect(run.state.calls.get(4)?.result?.content).toBe("ok");
380
+ expect(run.state.trace.find(4)?.status).toBe("completed");
381
+ });
382
+ });
@@ -0,0 +1,168 @@
1
+ // file-run-store-prune.test.ts —— FileRunStore.pruneStateFilesBeyondCap 磁盘保留测试(U7/C1/D8 + OR-5 ⑥b)。
2
+ //
3
+ // 语义对齐 pi jsonl-run-store mtime 裁剪实现(逐段平移):mtime 升序删最旧、
4
+ // 只碰 wf-*.jsonl、旁路容错(任何失败不抛)。
5
+ //
6
+ // 覆盖(验收条款②③ + OR-5 ⑥b 默认开):
7
+ // - 超 cap 裁剪最旧(mtime 递增序列,剩最新的 cap 个)
8
+ // - 未超 cap 不动
9
+ // - envName 语义(OR-5 ⑥b 默认开):env 未设/空 → 按默认上限 DEFAULT_STATE_MAX_RUNS
10
+ // 裁剪(修复前「默认关」即跨 run 无界累积缺陷本身);有效值 → 上限 = env 值;
11
+ // 非法值(非有限数/≤0)→ 不清理(显式 opt-out 通道)
12
+ // - 非 state 文件(不命中 glob)永不碰;目录不存在静默 no-op
13
+ //
14
+ // dataRoot 经 configureCore(tmp) 注入(对齐 file-run-store.test.ts 配置态隔离模式)。
15
+
16
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
+
22
+ import { configureCore, resetCoreForTests, type HostServices } from "../../core/host-services.ts";
23
+ import { DEFAULT_STATE_MAX_RUNS, FileRunStore } from "../file-run-store.ts";
24
+
25
+ const ENV_NAME = "XYZ_SUBAGENT_STATE_MAX_RUNS";
26
+
27
+ let dataRoot: string;
28
+ let store: FileRunStore;
29
+
30
+ beforeEach(() => {
31
+ resetCoreForTests();
32
+ dataRoot = mkdtempSync(join(tmpdir(), "file-run-store-prune-"));
33
+ const host: HostServices = {
34
+ dataRoot: () => dataRoot,
35
+ log: () => {},
36
+ };
37
+ configureCore(host);
38
+ store = new FileRunStore();
39
+ delete process.env[ENV_NAME];
40
+ });
41
+
42
+ afterEach(() => {
43
+ resetCoreForTests();
44
+ rmSync(dataRoot, { recursive: true, force: true });
45
+ delete process.env[ENV_NAME];
46
+ });
47
+
48
+ /** 往 workflow-state 目录写 state 文件并设定 mtime(秒级间距保证严格序)。 */
49
+ function writeStateFile(name: string, mtimeMs?: number): string {
50
+ const dir = join(dataRoot, "workflow-state");
51
+ mkdirSync(dir, { recursive: true });
52
+ const full = join(dir, name);
53
+ writeFileSync(full, `{"runId":"${name}"}\n`);
54
+ if (mtimeMs !== undefined) {
55
+ utimesSync(full, mtimeMs / 1000, mtimeMs / 1000);
56
+ }
57
+ return full;
58
+ }
59
+
60
+ /** 目录内文件名集合(断言终态)。 */
61
+ function listDir(): string[] {
62
+ return readdirSync(join(dataRoot, "workflow-state")).sort();
63
+ }
64
+
65
+ /** 写 N 个 mtime 递增的 state 文件(wf-1..wf-N,mtime 间隔 1s,wf-N 最新)。 */
66
+ function writeDatedStateFiles(n: number, baseMs: number): void {
67
+ for (let i = 1; i <= n; i++) {
68
+ writeStateFile(`wf-${i}.jsonl`, baseMs + i * 1000);
69
+ }
70
+ }
71
+
72
+ describe("FileRunStore.pruneStateFilesBeyondCap — max 直接裁剪", () => {
73
+ it("超 cap:mtime 升序删最旧,剩最新的 cap 个", async () => {
74
+ writeDatedStateFiles(5, 1_000_000);
75
+
76
+ await store.pruneStateFilesBeyondCap(3);
77
+
78
+ // wf-1/2 最旧先删,剩 mtime 最新的 3 个
79
+ expect(listDir()).toEqual(["wf-3.jsonl", "wf-4.jsonl", "wf-5.jsonl"]);
80
+ });
81
+
82
+ it("未超 cap:目录原样不动", async () => {
83
+ writeDatedStateFiles(3, 1_000_000);
84
+
85
+ await store.pruneStateFilesBeyondCap(10);
86
+
87
+ expect(listDir()).toEqual(["wf-1.jsonl", "wf-2.jsonl", "wf-3.jsonl"]);
88
+ });
89
+
90
+ it("恰等于 cap:no-op(边界 ≤ 判定)", async () => {
91
+ writeDatedStateFiles(3, 1_000_000);
92
+
93
+ await store.pruneStateFilesBeyondCap(3);
94
+
95
+ expect(listDir()).toEqual(["wf-1.jsonl", "wf-2.jsonl", "wf-3.jsonl"]);
96
+ });
97
+
98
+ it("只碰命中 wf-*.jsonl glob 的文件,非 state 文件永不删", async () => {
99
+ writeDatedStateFiles(4, 1_000_000);
100
+ writeStateFile("readme.txt", 900_000); // 最旧但非 state 文件
101
+ writeStateFile("other.jsonl", 950_000); // 不带 wf- 前缀
102
+
103
+ await store.pruneStateFilesBeyondCap(2);
104
+
105
+ // wf-1/2 被删;readme.txt 与 other.jsonl 虽更旧但不在 glob 内
106
+ expect(listDir()).toEqual(["other.jsonl", "readme.txt", "wf-3.jsonl", "wf-4.jsonl"]);
107
+ });
108
+
109
+ it("目录不存在(从未持久化):静默 no-op 不抛", async () => {
110
+ await expect(store.pruneStateFilesBeyondCap(3)).resolves.toBeUndefined();
111
+ });
112
+ });
113
+
114
+ describe("FileRunStore.pruneStateFilesBeyondCap — envName 语义(OR-5 ⑥b 默认开)", () => {
115
+ it("env 未设:按默认上限 DEFAULT_STATE_MAX_RUNS 裁剪(默认开,OR-5 修复)", async () => {
116
+ writeDatedStateFiles(DEFAULT_STATE_MAX_RUNS + 1, 1_000_000);
117
+
118
+ await store.pruneStateFilesBeyondCap(2, ENV_NAME);
119
+
120
+ // 默认上限生效:51 → 50,最旧的 wf-1 被删(修复前此处为 no-op = 无界累积)
121
+ const rest = listDir();
122
+ expect(rest).toHaveLength(DEFAULT_STATE_MAX_RUNS);
123
+ expect(rest).not.toContain("wf-1.jsonl");
124
+ expect(rest).toContain(`wf-${DEFAULT_STATE_MAX_RUNS + 1}.jsonl`);
125
+ });
126
+
127
+ it("env 设空串:视同未设,按默认上限裁剪", async () => {
128
+ writeDatedStateFiles(DEFAULT_STATE_MAX_RUNS + 1, 1_000_000);
129
+ process.env[ENV_NAME] = "";
130
+
131
+ await store.pruneStateFilesBeyondCap(2, ENV_NAME);
132
+
133
+ expect(listDir()).toHaveLength(DEFAULT_STATE_MAX_RUNS);
134
+ });
135
+
136
+ it("env 设有效值:上限 = env 值(覆盖默认值与 max 参数)", async () => {
137
+ writeDatedStateFiles(5, 1_000_000);
138
+ process.env[ENV_NAME] = "2";
139
+
140
+ // max=4 传入但 env=2 优先 → 裁到 2
141
+ await store.pruneStateFilesBeyondCap(4, ENV_NAME);
142
+
143
+ expect(listDir()).toEqual(["wf-4.jsonl", "wf-5.jsonl"]);
144
+ });
145
+
146
+ it("env 显式非法值(非有限数 / 0 / 负数):不清理(显式 opt-out 通道)", async () => {
147
+ for (const bad of ["abc", "0", "-1"]) {
148
+ writeDatedStateFiles(4, 1_000_000);
149
+ process.env[ENV_NAME] = bad;
150
+
151
+ await store.pruneStateFilesBeyondCap(2, ENV_NAME);
152
+
153
+ expect(listDir()).toEqual(["wf-1.jsonl", "wf-2.jsonl", "wf-3.jsonl", "wf-4.jsonl"]);
154
+ for (const f of readdirSync(join(dataRoot, "workflow-state"))) {
155
+ rmSync(join(dataRoot, "workflow-state", f));
156
+ }
157
+ delete process.env[ENV_NAME];
158
+ }
159
+ });
160
+
161
+ it("envName 缺省:直接按 max 裁剪(调用方自管启用时机,env 无关)", async () => {
162
+ writeDatedStateFiles(4, 1_000_000);
163
+
164
+ await store.pruneStateFilesBeyondCap(1);
165
+
166
+ expect(listDir()).toEqual(["wf-4.jsonl"]);
167
+ });
168
+ });