@zhushanwen/subagent-core 0.6.0 → 0.7.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 (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,200 @@
1
+ // src/execution/__tests__/idle-gc.test.ts
2
+ //
3
+ // [W4] idle-gc 扩展单测:startedAt 锚归档(无 idleSince 的 resumable record)、
4
+ // 只归档不补注销(archive 不发 pending:unregister——注销统一交对账 sweep)、
5
+ // WorkflowRun store 纳入(startedAt 锚终态化 + save)。fake timers 推进 GC
6
+ // interval;RecordStore 用 mkdtemp 自建目录 + pi=null(archive 纯内存,零磁盘写)。
7
+
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+
14
+ import { createRecord } from "../execution-record.ts";
15
+ import { startIdleGc } from "../idle-gc.ts";
16
+ import { RecordStore } from "../record-store.ts";
17
+ import type { ExecutionRecord } from "../types.ts";
18
+
19
+ const DAY_MS = 24 * 60 * 60 * 1000;
20
+ const GC_INTERVAL_MS = 60 * 60 * 1000;
21
+
22
+ let tmpDir: string;
23
+ let stop: (() => void) | undefined;
24
+
25
+ beforeEach(() => {
26
+ vi.useFakeTimers();
27
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "idle-gc-"));
28
+ });
29
+
30
+ afterEach(() => {
31
+ stop?.();
32
+ stop = undefined;
33
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
34
+ vi.useRealTimers();
35
+ });
36
+
37
+ function makeStore(): RecordStore {
38
+ return new RecordStore(path.join(tmpDir, "sessions"));
39
+ }
40
+
41
+ function makeRecord(id: string, overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
42
+ const record = createRecord(id, {
43
+ agent: "worker",
44
+ model: "m",
45
+ mode: "background",
46
+ task: "t",
47
+ slug: "s",
48
+ startedAt: Date.now(),
49
+ rootSessionId: "sess-root",
50
+ });
51
+ record.resumable = true;
52
+ Object.assign(record, overrides);
53
+ return record;
54
+ }
55
+
56
+ describe("idle-gc record 锚扩展(W4)", () => {
57
+ it("无 idleSince 的 resumable record 以 startedAt 为锚:超 30 天归档", async () => {
58
+ const store = makeStore();
59
+ const stale = makeRecord("bg-old", { startedAt: Date.now() - 31 * DAY_MS });
60
+ store.register(stale);
61
+ const fresh = makeRecord("bg-new", { startedAt: Date.now() - 1 * DAY_MS });
62
+ store.register(fresh);
63
+ const unregisterEmit = vi.fn();
64
+ stop = startIdleGc(store, undefined);
65
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
66
+ expect(store.getMutable("bg-old")).toBeUndefined(); // 已归档(内存移除)
67
+ expect(store.getMutable("bg-new")).toBeDefined();
68
+ expect(unregisterEmit).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("有 idleSince 的 resumable record 仍以 idleSince 为锚(现状语义保持)", async () => {
72
+ const store = makeStore();
73
+ const rec = makeRecord("bg-1", {
74
+ startedAt: Date.now() - 1 * DAY_MS,
75
+ idleSince: Date.now() - 31 * DAY_MS,
76
+ });
77
+ store.register(rec);
78
+ stop = startIdleGc(store);
79
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
80
+ expect(store.getMutable("bg-1")).toBeUndefined();
81
+ });
82
+
83
+ it("锚窗内(30 天量级以内)不归档", async () => {
84
+ const store = makeStore();
85
+ const rec = makeRecord("bg-1", { startedAt: Date.now() - 29 * DAY_MS });
86
+ store.register(rec);
87
+ stop = startIdleGc(store);
88
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
89
+ expect(store.getMutable("bg-1")).toBeDefined();
90
+ });
91
+
92
+ it("只归档不补注销:archive 不发 pending:unregister(store pi=null 且不注入 emit 通道)", async () => {
93
+ const store = makeStore();
94
+ let appendCalls = 0;
95
+ // pi=null 构造后再注入 spy 形态的 pi——archive 的唯一出站面是 pi.appendEntry
96
+ //(reportSubagentRecord)。归档必须零注销发射(发射点枚举 5 处不含 GC)。
97
+ const rec = makeRecord("bg-1", { startedAt: Date.now() - 31 * DAY_MS });
98
+ store.register(rec);
99
+ store.setPi({ appendEntry: () => { appendCalls += 1; } });
100
+ stop = startIdleGc(store);
101
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
102
+ expect(store.getMutable("bg-1")).toBeUndefined();
103
+ // archive 自身的 subagent-record entry 上报不构成注销;断言无 pending:unregister
104
+ // 形态调用由「startIdleGc 签名不持有 pi/emit 通道」结构性保证(类型层)。
105
+ expect(appendCalls).toBeGreaterThanOrEqual(0);
106
+ });
107
+ });
108
+
109
+ describe("idle-gc WorkflowRun store 纳入(W4)", () => {
110
+ function makeWorkflowStore(runs: Array<{
111
+ runId: string;
112
+ status: string;
113
+ startedAt: string;
114
+ }>) {
115
+ const transitions: Array<{ runId: string; reason?: string }> = [];
116
+ const saved: string[] = [];
117
+ return {
118
+ store: {
119
+ loadAll: async () =>
120
+ runs.map((r) => ({
121
+ runId: r.runId,
122
+ state: { status: r.status },
123
+ meta: { startedAt: r.startedAt },
124
+ transition: (_target: "done", reason?: string) => {
125
+ transitions.push({ runId: r.runId, reason });
126
+ },
127
+ })),
128
+ save: async (run: { runId: string }) => {
129
+ saved.push(run.runId);
130
+ },
131
+ },
132
+ transitions,
133
+ saved,
134
+ };
135
+ }
136
+
137
+ it("running 且 startedAt 超 30 天 → transition(done, time_limited) + save", async () => {
138
+ const { store, transitions, saved } = makeWorkflowStore([
139
+ { runId: "wf-old", status: "running", startedAt: new Date(Date.now() - 31 * DAY_MS).toISOString() },
140
+ ]);
141
+ const recordStore = makeStore();
142
+ stop = startIdleGc(recordStore, store);
143
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
144
+ expect(transitions).toEqual([{ runId: "wf-old", reason: "time_limited" }]);
145
+ expect(saved).toEqual(["wf-old"]);
146
+ });
147
+
148
+ it("窗内 running / 已终态 run 不动", async () => {
149
+ const { store, transitions, saved } = makeWorkflowStore([
150
+ { runId: "wf-new", status: "running", startedAt: new Date(Date.now() - 1 * DAY_MS).toISOString() },
151
+ { runId: "wf-done", status: "done", startedAt: new Date(Date.now() - 40 * DAY_MS).toISOString() },
152
+ ]);
153
+ stop = startIdleGc(makeStore(), store);
154
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
155
+ expect(transitions).toEqual([]);
156
+ expect(saved).toEqual([]);
157
+ });
158
+
159
+ it("loadAll 抛错(宿主未 configureCore)→ 单轮跳过不炸 interval", async () => {
160
+ const failing = {
161
+ loadAll: async () => {
162
+ throw new Error("core_host_not_configured");
163
+ },
164
+ save: async () => {},
165
+ };
166
+ stop = startIdleGc(makeStore(), failing);
167
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS * 2 + 1);
168
+ // 两次扫描周期都存活(未抛出即通过)。
169
+ });
170
+
171
+ it("save 抛错 → 吞错留痕,不阻断其余 run", async () => {
172
+ const transitions: Array<{ runId: string; reason?: string }> = [];
173
+ const failing = {
174
+ loadAll: async () => [
175
+ {
176
+ runId: "wf-a",
177
+ state: { status: "running" },
178
+ meta: { startedAt: new Date(Date.now() - 31 * DAY_MS).toISOString() },
179
+ transition: (_t: "done", reason?: string) => {
180
+ transitions.push({ runId: "wf-a", reason });
181
+ },
182
+ },
183
+ {
184
+ runId: "wf-b",
185
+ state: { status: "running" },
186
+ meta: { startedAt: new Date(Date.now() - 32 * DAY_MS).toISOString() },
187
+ transition: (_t: "done", reason?: string) => {
188
+ transitions.push({ runId: "wf-b", reason });
189
+ },
190
+ },
191
+ ],
192
+ save: async (run: { runId: string }) => {
193
+ if (run.runId === "wf-a") throw new Error("EIO");
194
+ },
195
+ };
196
+ stop = startIdleGc(makeStore(), failing);
197
+ await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
198
+ expect(transitions.map((t) => t.runId).sort()).toEqual(["wf-a", "wf-b"]);
199
+ });
200
+ });
@@ -1,7 +1,9 @@
1
- // lifecycle-predicates 单测(v4 B-1)。
1
+ // lifecycle-predicates 单测(v4 B-1;W6 随拆依赖改写:读点改 core 侧状态镜像)。
2
2
  // 验证 isIdle/isResumable/hasLiveProcessHandle 在两态收敛后的判定逻辑。
3
- // 依赖 lifecycle-manager 模块级 idleTimers 与 session-runner.spawnedChildren(单例 Map),
4
- // beforeEach 重置隔离。
3
+ // 依赖 lifecycle-manager 模块级 idleTimers 与 core 侧 spawnedChildren 镜像
4
+ // (engine/host/spawned-children.ts),beforeEach 重置隔离。
5
+ // [W3 改写] inproc 过渡桥并读用例随 inproc pi 引擎目录 删除消亡——镜像成为唯一数据源
6
+ // (host/childSpawned 上报 + 终止意图置死位),等价性用例的「并读」语义不再存在。
5
7
 
6
8
  import { describe, it, expect, beforeEach } from "vitest";
7
9
 
@@ -10,8 +12,10 @@ import {
10
12
  disarmIdleTimer,
11
13
  _resetLifecycleState,
12
14
  } from "../lifecycle-manager.ts";
13
- import { spawnedChildren } from "../engine/engines/pi/session-runner.ts";
14
- import type { ChildProcess } from "node:child_process";
15
+ import {
16
+ coreSpawnedChildrenMirror,
17
+ _resetCoreSpawnedChildrenMirrorForTest,
18
+ } from "../engine/host/spawned-children.ts";
15
19
  import type { ExecutionRecord } from "../types.ts";
16
20
 
17
21
  import { hasLiveProcessHandle, isIdle, isResumable } from "../lifecycle-predicates.ts";
@@ -44,16 +48,10 @@ function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
44
48
  } as ExecutionRecord;
45
49
  }
46
50
 
47
- /** 构造最小 ChildProcess fake(hasLiveProcessHandle 只读 .killed)。 */
48
- function fakeChild(killed: boolean): ChildProcess {
49
- // 测试 fake:被测代码仅读 .killed,其余字段不可达,用 unknown 中转满足 Map 类型。
50
- return { killed } as unknown as ChildProcess;
51
- }
52
-
53
51
  describe("lifecycle-predicates (v4 B-1)", () => {
54
52
  beforeEach(() => {
55
53
  _resetLifecycleState();
56
- spawnedChildren.clear();
54
+ _resetCoreSpawnedChildrenMirrorForTest();
57
55
  });
58
56
 
59
57
  describe("isIdle (= hasIdleTimer)", () => {
@@ -75,20 +73,27 @@ describe("lifecycle-predicates (v4 B-1)", () => {
75
73
  });
76
74
  });
77
75
 
78
- describe("hasLiveProcessHandle", () => {
79
- it("no child in map → false", () => {
76
+ describe("hasLiveProcessHandle(W6 改读镜像)", () => {
77
+ it("no mirror entry → false", () => {
80
78
  expect(hasLiveProcessHandle("sa-test")).toBe(false);
81
79
  });
82
80
 
83
- it("child present and not killed → true", () => {
84
- spawnedChildren.set("sa-test", fakeChild(false));
81
+ it("mirror entry running (not killed) → true", () => {
82
+ coreSpawnedChildrenMirror().register("sa-test", { pid: 1, killed: false });
85
83
  expect(hasLiveProcessHandle("sa-test")).toBe(true);
86
84
  });
87
85
 
88
- it("child present but killed → false", () => {
89
- spawnedChildren.set("sa-test", fakeChild(true));
86
+ it("mirror entry killed → false", () => {
87
+ coreSpawnedChildrenMirror().register("sa-test", { pid: 1, killed: true });
90
88
  expect(hasLiveProcessHandle("sa-test")).toBe(false);
91
89
  });
90
+
91
+ it("mirror markKilled 置死后 → false", () => {
92
+ coreSpawnedChildrenMirror().register("sa-test", { pid: 1, killed: false });
93
+ coreSpawnedChildrenMirror().markKilled("sa-test");
94
+ expect(hasLiveProcessHandle("sa-test")).toBe(false);
95
+ });
96
+
92
97
  });
93
98
 
94
99
  describe("isResumable (= running && !hasLiveProcessHandle)", () => {
@@ -96,9 +101,9 @@ describe("lifecycle-predicates (v4 B-1)", () => {
96
101
  expect(isResumable(makeRecord({ status: "running" }))).toBe(true);
97
102
  });
98
103
 
99
- it("running + live process → false (Path A 保活 / 正在执行)", () => {
104
+ it("running + live process (镜像) → false (Path A 保活 / 正在执行)", () => {
100
105
  const rec = makeRecord({ status: "running" });
101
- spawnedChildren.set(rec.id, fakeChild(false));
106
+ coreSpawnedChildrenMirror().register(rec.id, { pid: 1, killed: false });
102
107
  expect(isResumable(rec)).toBe(false);
103
108
  });
104
109
 
@@ -108,7 +113,7 @@ describe("lifecycle-predicates (v4 B-1)", () => {
108
113
 
109
114
  it("closed + live process → false (终态优先)", () => {
110
115
  const rec = makeRecord({ status: "closed" });
111
- spawnedChildren.set(rec.id, fakeChild(false));
116
+ coreSpawnedChildrenMirror().register(rec.id, { pid: 1, killed: false });
112
117
  expect(isResumable(rec)).toBe(false);
113
118
  });
114
119
  });
@@ -20,8 +20,9 @@ import {
20
20
  finalizedMarkerModule,
21
21
  fsSyncModule,
22
22
  manifestStoreModule,
23
- tempPromptModule,
24
23
  } from "./helpers/subagent-service-mocks.ts";
24
+ import { registerFakePiEngine } from "./helpers/fake-engine-port.ts";
25
+ import { clearEngines } from "../engine/registry.ts";
25
26
 
26
27
  // ── mock modules ──
27
28
 
@@ -30,7 +31,6 @@ vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<t
30
31
  vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
31
32
  vi.mock("../finalized-marker.ts", () => finalizedMarkerModule());
32
33
  vi.mock("../manifest-store.ts", () => manifestStoreModule());
33
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => tempPromptModule());
34
34
 
35
35
  import { spawn } from "node:child_process";
36
36
 
@@ -132,6 +132,9 @@ describe("TC-3: PI_SUBAGENT_ROOT_SESSION_ID env 贯穿正确", () => {
132
132
  });
133
133
 
134
134
  it("TC-3d: 子进程 execute 创建的 record.rootSessionId = env 贯穿的真 ROOT", async () => {
135
+ // [W3] 协议替身挂起(engine.run 不 settle)——record 保持 running 在 store 可观察
136
+ clearEngines();
137
+ registerFakePiEngine();
135
138
  process.env[ENV_ROOT_SESSION_ID] = "root-main-session";
136
139
  process.env[ENV_SELF_RECORD_ID] = "sa-parent";
137
140
  process.env[ENV_DEPTH] = "1";
@@ -0,0 +1,200 @@
1
+ // src/execution/__tests__/reconcile-sweep.test.ts
2
+ //
3
+ // [W4] 注册对账 sweep 单测:差集判据(record 终态 ∪ 已归档/不存在 → 补发;
4
+ // active → 跳过)、类型分流([F2]:workflow/畸形走 workflow run 判据——终态 ∪
5
+ // store 查不到 → 补注销,running → 跳过;bash 无收口通道保守跳过——显式偏差;
6
+ // 未注入 workflow 判据时保守跳过)、写法(appendEntry 权威落盘 + 尽力 emit;
7
+ // appendEntry 抛错不计入且不 emit)。差集输入 session 文件用
8
+ // mkdtempSync 自建自删(禁触真实数据目录)。
9
+
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+
16
+ import { runReconcileSweep, type SupervisedRecordState } from "../round-supervisor/reconcile-sweep.ts";
17
+
18
+ let tmpDir: string;
19
+ let sessionFile: string;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "round-sweep-"));
23
+ sessionFile = path.join(tmpDir, "session.jsonl");
24
+ });
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ function writeSessionFile(lines: unknown[]): void {
32
+ fs.writeFileSync(sessionFile, lines.map((l) => JSON.stringify(l)).join("\n") + "\n", "utf-8");
33
+ }
34
+
35
+ function reg(id: string, type = "subagent"): unknown {
36
+ return { customType: "pending:register", data: { id, type, name: id, registeredAt: 1, sessionId: "sess" } };
37
+ }
38
+
39
+ function unreg(id: string): unknown {
40
+ return { customType: "pending:unregister", data: { id, reason: "completed" } };
41
+ }
42
+
43
+ function makeDeps(overrides: {
44
+ states?: Map<string, SupervisedRecordState>;
45
+ workflowStates?: Map<string, SupervisedRecordState>;
46
+ injectWorkflowLookup?: boolean;
47
+ appendEntry?: (customType: string, data: unknown) => void;
48
+ emit?: (channel: string, data: unknown) => void;
49
+ sessionFile?: string;
50
+ } = {}) {
51
+ const states = overrides.states ?? new Map<string, SupervisedRecordState>();
52
+ const workflowStates = overrides.workflowStates;
53
+ const appended: Array<{ customType: string; data: unknown }> = [];
54
+ const emitted: Array<{ channel: string; data: unknown }> = [];
55
+ const deps = {
56
+ sessionFile: overrides.sessionFile ?? sessionFile,
57
+ lookupRecordState: (id: string): SupervisedRecordState => states.get(id) ?? "missing",
58
+ // [F2] workflow 判据:显式注入(injectWorkflowLookup !== false 或给了
59
+ // workflowStates)时按 Map 查;否则缺席(向后兼容形态)。
60
+ ...(overrides.injectWorkflowLookup === false && workflowStates === undefined
61
+ ? {}
62
+ : {
63
+ lookupWorkflowRunState: (id: string): SupervisedRecordState =>
64
+ workflowStates?.get(id) ?? "missing",
65
+ }),
66
+ appendEntry: overrides.appendEntry ?? ((customType: string, data: unknown) => appended.push({ customType, data })),
67
+ emit: overrides.emit ?? ((channel: string, data: unknown) => emitted.push({ channel, data })),
68
+ };
69
+ return { deps, appended, emitted };
70
+ }
71
+
72
+ describe("runReconcileSweep 差集补发", () => {
73
+ it("register(subagent) × record 终态 → 补发 unregister(appendEntry 权威 + 尽力 emit),reason 取 closedReason", () => {
74
+ writeSessionFile([reg("bg-1")]);
75
+ const { deps, appended, emitted } = makeDeps({
76
+ states: new Map([["bg-1", { terminal: true, closedReason: "cancelled" }]]),
77
+ });
78
+ const result = runReconcileSweep(deps);
79
+ expect(result.reconciled).toEqual(["bg-1"]);
80
+ expect(appended).toEqual([
81
+ { customType: "pending:unregister", data: { id: "bg-1", reason: "cancelled", status: "cancelled" } },
82
+ ]);
83
+ expect(emitted).toHaveLength(1);
84
+ });
85
+
86
+ it("record 已归档/不存在(查不到)→ 视同终态补注销(reason=expired)", () => {
87
+ writeSessionFile([reg("bg-gone")]);
88
+ const { deps, appended } = makeDeps({ states: new Map() }); // lookup 恒 missing
89
+ const result = runReconcileSweep(deps);
90
+ expect(result.reconciled).toEqual(["bg-gone"]);
91
+ expect(appended[0].data).toEqual({ id: "bg-gone", reason: "expired", status: "expired" });
92
+ });
93
+
94
+ it("record 活跃 → 保守跳过(不写不 emit)", () => {
95
+ writeSessionFile([reg("bg-live")]);
96
+ const { deps, appended, emitted } = makeDeps({ states: new Map([["bg-live", "active"]]) });
97
+ const result = runReconcileSweep(deps);
98
+ expect(result.skippedActive).toEqual(["bg-live"]);
99
+ expect(appended).toHaveLength(0);
100
+ expect(emitted).toHaveLength(0);
101
+ });
102
+
103
+ it("[F2] type=workflow × workflow run 终态 → 补注销(reason 取 run state reason)", () => {
104
+ writeSessionFile([reg("wf-1", "workflow")]);
105
+ const { deps, appended, emitted } = makeDeps({
106
+ workflowStates: new Map([["wf-1", { terminal: true, closedReason: "aborted" }]]),
107
+ });
108
+ const result = runReconcileSweep(deps);
109
+ expect(result.reconciled).toEqual(["wf-1"]);
110
+ expect(appended).toEqual([
111
+ { customType: "pending:unregister", data: { id: "wf-1", reason: "aborted", status: "aborted" } },
112
+ ]);
113
+ expect(emitted).toHaveLength(1);
114
+ });
115
+
116
+ it("[F2] type=workflow × workflow run running → 保守跳过(宁挂账不误注销活跃 run)", () => {
117
+ writeSessionFile([reg("wf-live", "workflow")]);
118
+ const { deps, appended } = makeDeps({
119
+ workflowStates: new Map([["wf-live", "active"]]),
120
+ });
121
+ const result = runReconcileSweep(deps);
122
+ expect(result.skippedActive).toEqual(["wf-live"]);
123
+ expect(result.reconciled).toEqual([]);
124
+ expect(appended).toHaveLength(0);
125
+ });
126
+
127
+ it("[F2] 畸形条目(type 缺失/未知,normalizePendingType 归 workflow)× store 查不到 → 视同终态补注销", () => {
128
+ writeSessionFile([reg("bad-1", "mystery")]);
129
+ const { deps, appended } = makeDeps({}); // workflowStates 缺省 = 恒 missing
130
+ const result = runReconcileSweep(deps);
131
+ expect(result.reconciled).toEqual(["bad-1"]);
132
+ expect(appended[0].data).toEqual({ id: "bad-1", reason: "expired", status: "expired" });
133
+ });
134
+
135
+ it("[F2] type=bash → 保守跳过(无 record/store 可查,显式偏差——宁挂账不失明)", () => {
136
+ writeSessionFile([reg("bt-1", "bash")]);
137
+ const { deps, appended } = makeDeps({});
138
+ const result = runReconcileSweep(deps);
139
+ expect(result.skippedNonSubagent).toEqual(["bt-1"]);
140
+ expect(appended).toHaveLength(0);
141
+ });
142
+
143
+ it("[F2] deps 未注入 workflow 判据 → workflow/未知类型保守跳过(判据缺席 ≠ 可注销)", () => {
144
+ writeSessionFile([reg("wf-1", "workflow"), reg("bad-1", "mystery")]);
145
+ const { deps, appended } = makeDeps({ injectWorkflowLookup: false });
146
+ const result = runReconcileSweep(deps);
147
+ expect(result.skippedNonSubagent.sort()).toEqual(["bad-1", "wf-1"]);
148
+ expect(appended).toHaveLength(0);
149
+ });
150
+
151
+ it("已注销的 id 不出现在差集 → 不补发", () => {
152
+ writeSessionFile([reg("bg-1"), unreg("bg-1")]);
153
+ const { deps, appended } = makeDeps();
154
+ const result = runReconcileSweep(deps);
155
+ expect(result.reconciled).toEqual([]);
156
+ expect(appended).toHaveLength(0);
157
+ });
158
+
159
+ it("unregister 之后的重注册重新进差集(同 id 重 register = 后写胜出)", () => {
160
+ writeSessionFile([reg("bg-1"), unreg("bg-1"), reg("bg-1")]);
161
+ const { deps, appended } = makeDeps();
162
+ const result = runReconcileSweep(deps);
163
+ expect(result.reconciled).toEqual(["bg-1"]);
164
+ expect(appended).toHaveLength(1);
165
+ });
166
+
167
+ it("sessionFile 缺失 → 空跑(不抛)", () => {
168
+ const { deps } = makeDeps({ sessionFile: undefined });
169
+ const result = runReconcileSweep(deps);
170
+ expect(result.reconciled).toEqual([]);
171
+ });
172
+
173
+ it("session 文件不可读(未 flush/被删)→ 空跑", () => {
174
+ const { deps } = makeDeps({ sessionFile: path.join(tmpDir, "nope.jsonl") });
175
+ const result = runReconcileSweep(deps);
176
+ expect(result.reconciled).toEqual([]);
177
+ });
178
+
179
+ it("appendEntry 抛错 → 不计 reconciled、不 emit(差集残留交下次 sweep 重试)", () => {
180
+ writeSessionFile([reg("bg-1"), reg("bg-2")]);
181
+ const { deps, emitted } = makeDeps({
182
+ appendEntry: (customType, data) => {
183
+ if ((data as { id: string }).id === "bg-1") throw new Error("EACCES");
184
+ },
185
+ });
186
+ const result = runReconcileSweep(deps);
187
+ expect(result.reconciled).toEqual(["bg-2"]);
188
+ expect(emitted.map((e) => (e.data as { id: string }).id)).toEqual(["bg-2"]);
189
+ });
190
+
191
+ it("坏行(截断 JSON)跳过不拖垮其余条目", () => {
192
+ fs.writeFileSync(sessionFile, [JSON.stringify(reg("bg-1")), '{"customType":"pending:regis'].join("\n"), "utf-8");
193
+ const { deps, appended } = makeDeps({
194
+ states: new Map([["bg-1", { terminal: true, closedReason: "gc" }]]),
195
+ });
196
+ const result = runReconcileSweep(deps);
197
+ expect(result.reconciled).toEqual(["bg-1"]);
198
+ expect(appended).toHaveLength(1);
199
+ });
200
+ });
@@ -36,7 +36,7 @@ vi.mock("../sessions-index.ts", async (importOriginal) => {
36
36
  });
37
37
 
38
38
  import { writeAliveMarker } from "../alive-store.ts";
39
- import { completeRecord, createRecord, tryTransition } from "../execution-record.ts";
39
+ import { completeRecord, createRecord, projectOutcome, tryTransition } from "../execution-record.ts";
40
40
  import { writeFinalized } from "../finalized-marker.ts";
41
41
  import type { ManifestRecord } from "../manifest-store.ts";
42
42
  import { ManifestStore } from "../manifest-store.ts";
@@ -817,7 +817,7 @@ describe("RecordStore", () => {
817
817
  return { store, appended };
818
818
  }
819
819
 
820
- it("末行完整 closed 终态 entry + .finalized sidecar(防重锚)", () => {
820
+ it("末行完整(in-flight,无 resumable)→ closed + 重启中断 error,投影 failed([F3] 表 3 行 2)", () => {
821
821
  const sessionFile = path.join(tmpDir, "orphan-done.jsonl");
822
822
  writeSessionJsonl(sessionFile, {
823
823
  id: "sa-orphan-1", agent: "worker", mode: "background", task: "orphan done",
@@ -831,7 +831,14 @@ describe("RecordStore", () => {
831
831
  expect(entry?.data.status).toBe("closed");
832
832
  expect(entry?.data.closedReason).toBe("gc");
833
833
  expect(entry?.data.endedAt).toEqual(expect.any(Number));
834
- expect(entry?.data.error).toBeUndefined();
834
+ // [F3] in-flight 直断 = boot 直断 failed 语义:任务因宿主重启中断,不得投影
835
+ // completed(事故环 3 残留)。error 载体 → deriveOutcome("gc", error) = failed。
836
+ expect(entry?.data.error).toContain("host restart");
837
+ expect(projectOutcome({
838
+ status: "closed",
839
+ closedReason: entry?.data.closedReason as never,
840
+ error: entry?.data.error as string | undefined,
841
+ })).toBe("failed");
835
842
  expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
836
843
  // 防重:sidecar 使下次重建走分支 2(closed),不再进判定
837
844
  const again = [...appended];
@@ -841,7 +848,51 @@ describe("RecordStore", () => {
841
848
  expect(found?.status).toBe("closed");
842
849
  });
843
850
 
844
- it("末行截断 closed + error(保守方向)", () => {
851
+ it("SP-5 完成态(resumable + result 有值)→ completed 无 error(不误标重启失败,[F3] 不回归)", () => {
852
+ const sessionFile = path.join(tmpDir, "orphan-sp5.jsonl");
853
+ writeSessionJsonl(sessionFile, {
854
+ id: "sa-orphan-sp5", agent: "worker", mode: "background", task: "sp5 done",
855
+ startedAt: 6000, rootSessionId: "sess-orphan",
856
+ });
857
+ // 主 session 末条 entry(v2 D3 merge 数据源)携 resumable+result——W4 起轮终
858
+ // 执行态信号随 entry 保留,子文件侧重建不带该信号。data 需过 rebuildEntryRecord
859
+ // 形状守卫(agent/task/startedAt 必带)。
860
+ const mainFile = writeMainSession([
861
+ { id: "sa-orphan-sp5", agent: "worker", task: "sp5 done", startedAt: 6000, status: "running", resumable: true, result: "final answer text" },
862
+ ]);
863
+ const { store, appended } = makeRecoveryStore();
864
+ store.recoverOrphanRecords("sess-orphan", mainFile);
865
+
866
+ const entry = appended.find((c) => c.data.id === "sa-orphan-sp5");
867
+ expect(entry?.data.status).toBe("closed");
868
+ expect(entry?.data.error).toBeUndefined();
869
+ expect(projectOutcome({
870
+ status: "closed",
871
+ closedReason: entry?.data.closedReason as never,
872
+ error: entry?.data.error as string | undefined,
873
+ })).toBe("completed");
874
+ });
875
+
876
+ it("resumable 无产出 → 保持 running 落 resumable entry(W4 死亡纳管态跨重启,boot 重认领源,不直断)", () => {
877
+ const sessionFile = path.join(tmpDir, "orphan-resumable.jsonl");
878
+ writeSessionJsonl(sessionFile, {
879
+ id: "sa-orphan-res", agent: "worker", mode: "background", task: "resumable orphan",
880
+ startedAt: 7000, rootSessionId: "sess-orphan",
881
+ });
882
+ const mainFile = writeMainSession([
883
+ { id: "sa-orphan-res", agent: "worker", task: "resumable orphan", startedAt: 7000, status: "running", resumable: true },
884
+ ]);
885
+ const { store, appended } = makeRecoveryStore();
886
+ store.recoverOrphanRecords("sess-orphan", mainFile);
887
+
888
+ const entry = appended.find((c) => c.data.id === "sa-orphan-res");
889
+ expect(entry?.data.status).toBe("running");
890
+ expect(entry?.data.resumable).toBe(true);
891
+ // 保留分支不写终态 sidecar
892
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
893
+ });
894
+
895
+ it("末行截断 → closed + error(保守方向;[F3] in-flight 同时携重启中断语义)", () => {
845
896
  const sessionFile = path.join(tmpDir, "orphan-truncated.jsonl");
846
897
  writeSessionJsonl(sessionFile, {
847
898
  id: "sa-orphan-2", agent: "worker", mode: "background", task: "orphan truncated",
@@ -855,10 +906,11 @@ describe("RecordStore", () => {
855
906
  const entry = appended.find((c) => c.data.id === "sa-orphan-2");
856
907
  expect(entry?.data.status).toBe("closed");
857
908
  expect(entry?.data.error).toContain("truncated");
909
+ expect(entry?.data.error).toContain("host restart");
858
910
  expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
859
911
  });
860
912
 
861
- it("末行 >64KB 完整 JSON → 不误判截断,判 done(V1 探针实测回归)", () => {
913
+ it("末行 >64KB 完整 JSON → 不误判截断,[F3] in-flight 携重启中断 error(无 truncated)", () => {
862
914
  const sessionFile = path.join(tmpDir, "orphan-longline.jsonl");
863
915
  writeSessionJsonl(sessionFile, {
864
916
  id: "sa-orphan-5", agent: "worker", mode: "background", task: "orphan long line",
@@ -877,7 +929,9 @@ describe("RecordStore", () => {
877
929
 
878
930
  const entry = appended.find((c) => c.data.id === "sa-orphan-5");
879
931
  expect(entry?.data.status).toBe("closed");
880
- expect(entry?.data.error).toBeUndefined();
932
+ // [F3] in-flight 重启中断 error 必有;「不误判截断」= error 不含 truncated 标记
933
+ expect(entry?.data.error).toContain("host restart");
934
+ expect(entry?.data.error).not.toContain("truncated");
881
935
  expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
882
936
  });
883
937