@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,661 @@
1
+ // src/execution/round-supervisor/service-binding.test.ts
2
+ //
3
+ // [MF-1 round1 test-coverage] service-binding 绑定面直测(审查基线覆盖 23.3%)。
4
+ //
5
+ // 覆盖面:
6
+ // 1. supervisorGiveUp(C-proc-13 注销发射点④「监督器放弃」唯一执行者):
7
+ // - 内存态 watchdog-expired:CAS 终态化 + finalizeClosed failed result +
8
+ // 终止通知(「safe to re-dispatch」用户语义,经 steer 通道);
9
+ // - CAS 抢锁失败跳过(他人已终态化时不重复放弃——同步链内不可达的防御
10
+ // 分支,替身 getMutable 受控注入驱动契约);
11
+ // - finalizeClosed 异常 best-effort(不向上抛、终止通知仍发);
12
+ // - superseded:errorText 带替代 id、不发终止通知;
13
+ // - 磁盘态(boot 重认领后看门狗到期):writeFinalized sidecar 落盘 +
14
+ // reportSubagentRecord 终态 entry(closed/gc/endedAt/error)、无 pi 通知;
15
+ // - 磁盘态无 sessionFile / writeFinalized 抛错(sidecar catch)/ 双 miss 收尾
16
+ // 竞争 / reportSubagentRecord 抛错(entry catch,须先经 boot 重认领武装
17
+ // 看门狗——无候选则 giveUp 不执行,用例空转)。
18
+ // 2. runPendingReconcileSweepForService 的 lookupRecordState 闭包 subagent 判据
19
+ // (workflow 判据回归钉在 workflow-state-root.test.ts,此处补 subagent 两方向:
20
+ // 活跃 record 不被 sweep 注销 + 终态/missing 差集补发注销)。
21
+ // 3. createRoundSupervisorForService 通知装配(merged failure / decision guidance
22
+ // + 豁免声明 / replaced notice 全走 steer 通道;candidates 接线;record 视图
23
+ // 磁盘兜底投影)。
24
+ //
25
+ // 测试纪律:fake timers(watchdog 推进);替身 RecordStore/PiLike 驱动(不触真实
26
+ // 数据目录);sidecar/register 写点全部 mkdtempSync 自建自删;PI_CODING_AGENT_DIR
27
+ // stub 指向 tmp(sweep 内 FileRunStore 构造读侧解析不探测真实 ~/.pi/agent)。
28
+
29
+ import * as fs from "node:fs";
30
+ import * as os from "node:os";
31
+ import * as path from "node:path";
32
+
33
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
34
+
35
+ import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-resurrect.ts";
36
+ import * as finalizedMarker from "../finalized-marker.ts";
37
+ import type { PiLike } from "../notify-host.ts";
38
+ import type { RecordStore } from "../record-store.ts";
39
+ import type { ExecutionRecord, SubagentRecord } from "../types.ts";
40
+ import { RoundSupervisor, ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS, type SupervisorCandidateRecord } from "./index.ts";
41
+ import {
42
+ createRoundSupervisorForService,
43
+ runPendingReconcileSweepForService,
44
+ type RoundSupervisorBinding,
45
+ } from "./service-binding.ts";
46
+
47
+ // writeFinalized 实装自吞 IO 错(内部 try/catch,finalized-marker.ts),give-up
48
+ // sidecar 的 catch(service-binding.ts:164)是防御分支——真实 ENOENT 无法驱动,
49
+ // 仅本文件的 sidecar 抛错用例经模块替身注入抛错;默认委托真实实现,相邻用例行为
50
+ // 不变(vi.spyOn 对跨模块具名导入绑定不可拦截,必须走 vi.mock)。
51
+ vi.mock("../finalized-marker.ts", async (importOriginal) => {
52
+ const actual = await importOriginal<typeof import("../finalized-marker.ts")>();
53
+ return { ...actual, writeFinalized: vi.fn(actual.writeFinalized) };
54
+ });
55
+
56
+ // ============================================================
57
+ // 替身
58
+ // ============================================================
59
+
60
+ type SentMessage = {
61
+ message: { customType: string; content: string; display: boolean };
62
+ options: { deliverAs?: string } | undefined;
63
+ };
64
+
65
+ type TestPi = PiLike & {
66
+ sent: SentMessage[];
67
+ appended: Array<{ customType: string; data: unknown }>;
68
+ emitted: Array<{ channel: string; data: unknown }>;
69
+ };
70
+
71
+ function makePi(): TestPi {
72
+ const sent: SentMessage[] = [];
73
+ const appended: Array<{ customType: string; data: unknown }> = [];
74
+ const emitted: Array<{ channel: string; data: unknown }> = [];
75
+ return {
76
+ sendMessage: vi.fn((message: SentMessage["message"], options?: SentMessage["options"]) => {
77
+ sent.push({ message, options });
78
+ }),
79
+ appendEntry: vi.fn((customType: string, data?: unknown) => {
80
+ appended.push({ customType, data });
81
+ }),
82
+ events: {
83
+ emit: vi.fn((channel: string, data: unknown) => {
84
+ emitted.push({ channel, data });
85
+ }),
86
+ },
87
+ sent,
88
+ appended,
89
+ emitted,
90
+ } as unknown as TestPi;
91
+ }
92
+
93
+ interface StoreHarness {
94
+ store: RecordStore;
95
+ memory: Map<string, ExecutionRecord>;
96
+ disk: Map<string, SubagentRecord>;
97
+ /** collectRecords 返回的候选(替身投影;默认从 memory 生成)。 */
98
+ collectResult: SupervisorCandidateRecord[];
99
+ collectCalls: Array<{ limit: number; status: string; rootFilter: string | undefined }>;
100
+ reported: SubagentRecord[];
101
+ }
102
+
103
+ /** duck-typed RecordStore 替身(service-binding 只消费 getMutable / findLightById /
104
+ * collectRecords / reportSubagentRecord 四个读写点)。 */
105
+ function makeStore(): StoreHarness {
106
+ const memory = new Map<string, ExecutionRecord>();
107
+ const disk = new Map<string, SubagentRecord>();
108
+ const collectResult: SupervisorCandidateRecord[] = [];
109
+ const collectCalls: StoreHarness["collectCalls"] = [];
110
+ const reported: SubagentRecord[] = [];
111
+ const store = {
112
+ getMutable: (id: string) => memory.get(id),
113
+ findLightById: (id: string) => disk.get(id),
114
+ collectRecords: (limit: number, status: string, rootFilter: string | undefined) => {
115
+ collectCalls.push({ limit, status, rootFilter });
116
+ return collectResult as never;
117
+ },
118
+ reportSubagentRecord: (record: SubagentRecord) => {
119
+ reported.push(record);
120
+ },
121
+ };
122
+ return { store: store as unknown as RecordStore, memory, disk, collectResult, collectCalls, reported };
123
+ }
124
+
125
+ interface BindingHarness {
126
+ binding: RoundSupervisorBinding;
127
+ store: StoreHarness;
128
+ pi: ReturnType<typeof makePi> | null;
129
+ finalizeClosed: ReturnType<typeof vi.fn>;
130
+ }
131
+
132
+ function makeBinding(overrides: {
133
+ store?: StoreHarness;
134
+ pi?: ReturnType<typeof makePi> | null;
135
+ rootId?: string | null;
136
+ sessionFile?: string;
137
+ } = {}): BindingHarness {
138
+ const storeHarness = overrides.store ?? makeStore();
139
+ const pi = overrides.pi === undefined ? makePi() : overrides.pi;
140
+ const finalizeClosed = vi.fn(async () => {});
141
+ const binding: RoundSupervisorBinding = {
142
+ getStore: () => storeHarness.store,
143
+ getPi: () => pi,
144
+ getSessionRootId: () => (overrides.rootId === undefined ? "root-1" : overrides.rootId),
145
+ getMainSessionFile: () => overrides.sessionFile,
146
+ finalizeClosed: finalizeClosed as unknown as (r: ExecutionRecord, res: unknown) => Promise<void>,
147
+ };
148
+ return { binding, store: storeHarness, pi, finalizeClosed };
149
+ }
150
+
151
+ /** 内存 ExecutionRecord 替身(字段访问面:id/agent/slug/status/resumable/result/
152
+ * chatMode/rootSessionId/startedAt/closedReason/turnCount)。 */
153
+ function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
154
+ return {
155
+ id: "bg-1",
156
+ agent: "worker",
157
+ model: "m",
158
+ mode: "background",
159
+ task: "t",
160
+ slug: "fix-bug",
161
+ startedAt: Date.now() - 1000,
162
+ status: "running",
163
+ resumable: true,
164
+ rootSessionId: "root-1",
165
+ turnCount: 3,
166
+ ...overrides,
167
+ } as ExecutionRecord;
168
+ }
169
+
170
+ /** 磁盘 SubagentRecord 替身(findLightById 返回形态,最小字段集)。 */
171
+ function makeDiskRecord(sessionFile: string | undefined, overrides: Partial<SubagentRecord> = {}): SubagentRecord {
172
+ return {
173
+ id: "bg-disk",
174
+ agent: "worker",
175
+ task: "t",
176
+ slug: "fix-bug",
177
+ status: "running",
178
+ mode: "background",
179
+ startedAt: Date.now() - 2000,
180
+ rootSessionId: "root-1",
181
+ parentRecordId: undefined,
182
+ depth: 0,
183
+ endedAt: undefined,
184
+ turns: 1,
185
+ totalTokens: 0,
186
+ model: "m",
187
+ eventLog: [],
188
+ displayItems: [],
189
+ resumable: true,
190
+ sessionFile,
191
+ ...overrides,
192
+ } as unknown as SubagentRecord;
193
+ }
194
+
195
+ // ============================================================
196
+ // 测试
197
+ // ============================================================
198
+
199
+ /** 终止通知文案锚(supervisorGiveUp watchdog-expired 分支独有;guidance/replaced/merged 均不含)。 */
200
+ const TERMINATION_NOTICE_MARK = "terminated after the decision watchdog";
201
+
202
+ let tmpDir: string;
203
+ let sessionFile: string;
204
+
205
+ beforeEach(() => {
206
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "svc-binding-"));
207
+ sessionFile = path.join(tmpDir, "main-session.jsonl");
208
+ vi.useFakeTimers();
209
+ });
210
+
211
+ afterEach(() => {
212
+ vi.useRealTimers();
213
+ vi.unstubAllEnvs();
214
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
215
+ });
216
+
217
+ /** 纳管 + 看门狗到期 → giveUp("watchdog-expired")(异步 giveUp 编排排空微任务)。 */
218
+ async function adoptThenExpire(supervisor: RoundSupervisor, record: ExecutionRecord): Promise<void> {
219
+ supervisor.adoptOnProcessDeath(record, "engine crashed");
220
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
221
+ }
222
+
223
+ describe("supervisorGiveUp 内存态(watchdog-expired / superseded / CAS)", () => {
224
+ it("watchdog-expired:CAS 终态化 + finalizeClosed 收 failed result + 终止通知经 steer(safe to re-dispatch 语义)", async () => {
225
+ const h = makeBinding({ sessionFile });
226
+ h.store.memory.set("bg-1", makeRecord());
227
+ const supervisor = createRoundSupervisorForService(h.binding);
228
+
229
+ await adoptThenExpire(supervisor, makeRecord());
230
+
231
+ // CAS 终态化已落(closed + gc),finalizeClosed 收失败语义 result
232
+ expect(h.store.memory.get("bg-1")?.status).toBe("closed");
233
+ expect(h.store.memory.get("bg-1")?.closedReason).toBe("gc");
234
+ expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
235
+ const [record, result] = (h.finalizeClosed.mock.calls[0] ?? []) as [ExecutionRecord, Record<string, unknown>];
236
+ expect(record.id).toBe("bg-1");
237
+ expect(result).toMatchObject({
238
+ text: "",
239
+ turns: 3,
240
+ success: false,
241
+ sessionId: "bg-1",
242
+ toolCalls: [],
243
+ durationMs: expect.any(Number),
244
+ error: expect.stringContaining("safe to re-dispatch"),
245
+ });
246
+
247
+ // 终止通知:steer 通道、customType/display 契约、agent/slug 用户语义
248
+ // (sent = [merged failure, decision guidance, termination] 三条,钉最后一条)
249
+ expect(h.pi?.sent).toHaveLength(3);
250
+ const sent = h.pi!.sent[2]!;
251
+ expect(sent.options).toEqual({ deliverAs: "steer" });
252
+ expect(sent.message.customType).toBe("subagent-round-supervisor");
253
+ expect(sent.message.display).toBe(false);
254
+ expect(sent.message.content).toContain('Subagent "worker" (fix-bug)');
255
+ expect(sent.message.content).toContain("decision watchdog");
256
+ expect(sent.message.content).toContain("you can safely re-dispatch the task");
257
+ });
258
+
259
+ it("CAS 抢锁失败(视图检查后 record 已被 cancel 终态化)→ 不 finalizeClosed、不通知、不重复放弃", async () => {
260
+ const h = makeBinding({ sessionFile });
261
+ const rec = makeRecord();
262
+ h.store.memory.set("bg-1", rec);
263
+ // 同步链内 view 检查与 CAS 之间不可插入并发终态化(防御分支)——替身受控注入:
264
+ // 第一次 getMutable(watchdog 回调的视图检查)返回 running,其后(giveUp 的
265
+ // memory 读取)返回已被 cancel 抢先终态化的同一 record。
266
+ let viewChecked = false;
267
+ h.store.store.getMutable = (id: string) => {
268
+ if (!viewChecked) {
269
+ viewChecked = true;
270
+ return rec;
271
+ }
272
+ return { ...rec, status: "closed", closedReason: "cancelled" } as ExecutionRecord;
273
+ };
274
+ const supervisor = createRoundSupervisorForService(h.binding);
275
+
276
+ await adoptThenExpire(supervisor, rec);
277
+
278
+ expect(h.finalizeClosed).not.toHaveBeenCalled();
279
+ // 纳管期通知(merged + guidance)照常,但终止通知不发生(不重复放弃)
280
+ expect(h.pi?.sent).toHaveLength(2);
281
+ expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
282
+ expect(h.store.reported).toHaveLength(0);
283
+ });
284
+
285
+ it("finalizeClosed 抛错 → best-effort 吞掉不向上抛,终止通知仍发", async () => {
286
+ const h = makeBinding({ sessionFile });
287
+ h.finalizeClosed.mockImplementation(async () => {
288
+ throw new Error("finalize exploded");
289
+ });
290
+ h.store.memory.set("bg-1", makeRecord());
291
+ const supervisor = createRoundSupervisorForService(h.binding);
292
+
293
+ await expect(adoptThenExpire(supervisor, makeRecord())).resolves.toBeUndefined();
294
+
295
+ expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
296
+ expect(h.pi?.sent).toHaveLength(3);
297
+ expect(h.pi?.sent[2]?.message.content).toContain("safely re-dispatch");
298
+ });
299
+
300
+ it("superseded(高置信替代):errorText 带替代 id,不发终止通知(替代通知由监督器先行)", async () => {
301
+ const h = makeBinding({ sessionFile });
302
+ h.store.memory.set("bg-1", makeRecord());
303
+ h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
304
+ const supervisor = createRoundSupervisorForService(h.binding);
305
+
306
+ await adoptThenExpire(supervisor, makeRecord());
307
+
308
+ const [record, result] = (h.finalizeClosed.mock.calls[0] ?? []) as [ExecutionRecord, Record<string, unknown>];
309
+ expect(record.id).toBe("bg-1");
310
+ expect(result.error).toBe("round supervisor: superseded by replacement task bg-2");
311
+ // 终止通知不发;sent = [merged failure, replaced notice] 两条
312
+ expect(h.pi?.sent).toHaveLength(2);
313
+ expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
314
+ expect(h.pi?.sent[1]?.message.content).toContain("was already replaced by your new");
315
+ });
316
+
317
+ it("pi 缺席(dispose 后 getPi null)→ 终态化照常、通知静默丢弃、不炸", async () => {
318
+ const h = makeBinding({ sessionFile, pi: null });
319
+ h.store.memory.set("bg-1", makeRecord());
320
+ const supervisor = createRoundSupervisorForService(h.binding);
321
+
322
+ await expect(adoptThenExpire(supervisor, makeRecord())).resolves.toBeUndefined();
323
+
324
+ expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
325
+ expect(h.store.memory.get("bg-1")?.status).toBe("closed");
326
+ });
327
+ });
328
+
329
+ describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", () => {
330
+ function makeDiskBinding(diskRecord: SubagentRecord): BindingHarness {
331
+ const h = makeBinding({ sessionFile });
332
+ h.store.disk.set(diskRecord.id, diskRecord);
333
+ // boot 分区候选(真实 collectRecords 的 running 投影形态)
334
+ h.store.collectResult.push({
335
+ id: diskRecord.id,
336
+ rootSessionId: diskRecord.rootSessionId,
337
+ agent: diskRecord.agent,
338
+ slug: diskRecord.slug,
339
+ startedAt: diskRecord.startedAt,
340
+ });
341
+ return h;
342
+ }
343
+
344
+ it("writeFinalized sidecar 落盘 + reportSubagentRecord 终态 entry(closed/gc/endedAt/error);磁盘态不发终止通知", async () => {
345
+ const diskRecord = makeDiskRecord(sessionFile);
346
+ const h = makeDiskBinding(diskRecord);
347
+ const supervisor = createRoundSupervisorForService(h.binding);
348
+
349
+ // 纳管经磁盘兜底视图(内存 Map 空)——boot 分区重认领形态
350
+ supervisor.bootPartition();
351
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
352
+
353
+ // sidecar:sessionFile 旁 .finalized 内容 = "gc";finalizeClosed 不适用(无内存 record)
354
+ expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
355
+ expect(h.finalizeClosed).not.toHaveBeenCalled();
356
+
357
+ // 终态 entry 落盘(reportSubagentRecord):磁盘 record + 终态语义位
358
+ expect(h.store.reported).toHaveLength(1);
359
+ const reported = h.store.reported[0]!;
360
+ expect(reported).toMatchObject({
361
+ id: "bg-disk",
362
+ status: "closed",
363
+ closedReason: "gc",
364
+ endedAt: expect.any(Number),
365
+ error: expect.stringContaining("safe to re-dispatch"),
366
+ });
367
+ expect(reported.error).not.toContain("superseded");
368
+
369
+ // 磁盘态分支无终止通知(sent 仅 boot 重认领的 decision guidance 一条)
370
+ expect(h.pi?.sent).toHaveLength(1);
371
+ expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
372
+ });
373
+
374
+ it("磁盘态无 sessionFile → 跳过 sidecar,仍 reportSubagentRecord 终态化", async () => {
375
+ const diskRecord = makeDiskRecord(undefined);
376
+ const h = makeDiskBinding(diskRecord);
377
+ const supervisor = createRoundSupervisorForService(h.binding);
378
+
379
+ supervisor.bootPartition();
380
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
381
+
382
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
383
+ expect(h.store.reported).toHaveLength(1);
384
+ expect(h.store.reported[0]?.status).toBe("closed");
385
+ });
386
+
387
+ it("reportSubagentRecord 抛错 → best-effort 吞掉不向上抛,sidecar 照常落盘", async () => {
388
+ // 候选入 collectResult → boot 重认领(readopted)武装看门狗——没有这一步
389
+ // listCandidateRecords 返回空、giveUp 永不执行、entry catch 不可达(MF-R2-2)。
390
+ const diskRecord = makeDiskRecord(sessionFile);
391
+ const h = makeDiskBinding(diskRecord);
392
+ h.store.store.reportSubagentRecord = () => {
393
+ throw new Error("entry write exploded");
394
+ };
395
+ const supervisor = createRoundSupervisorForService(h.binding);
396
+
397
+ const { readopted } = supervisor.bootPartition();
398
+ expect(readopted).toEqual([diskRecord.id]);
399
+ // 裸 await:giveUp 编排若有逃逸异常会以 unhandled rejection 判红本用例
400
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
401
+
402
+ // sidecar 与终态 entry 是两段独立 best-effort:entry 抛错不回滚已落 sidecar
403
+ expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
404
+ // 终态 entry 写入抛错被 give-up entry catch 吞掉 → reported 空
405
+ expect(h.store.reported).toHaveLength(0);
406
+ });
407
+
408
+ it("writeFinalized 抛错 → best-effort 吞掉,终态 entry 照常落盘", async () => {
409
+ const diskRecord = makeDiskRecord(path.join(tmpDir, "gone-dir", "s.jsonl"));
410
+ const h = makeDiskBinding(diskRecord);
411
+ const supervisor = createRoundSupervisorForService(h.binding);
412
+ // 模块替身注入抛错(见文件头 vi.mock 注释);finally 恢复委托真实实现。
413
+ const actual = await vi.importActual<typeof import("../finalized-marker.ts")>(
414
+ "../finalized-marker.ts",
415
+ );
416
+ const writeSpy = vi.mocked(finalizedMarker.writeFinalized);
417
+ writeSpy.mockImplementation(() => {
418
+ throw new Error("sidecar write exploded");
419
+ });
420
+ try {
421
+ supervisor.bootPartition();
422
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
423
+ } finally {
424
+ writeSpy.mockImplementation(actual.writeFinalized);
425
+ }
426
+
427
+ // 替身确实被 giveUp 消费(否则异常被 writeFinalized 实装自吞,用例假绿)
428
+ expect(writeSpy).toHaveBeenCalled();
429
+ expect(fs.existsSync(`${diskRecord.sessionFile}.finalized`)).toBe(false);
430
+ expect(h.store.reported).toHaveLength(1);
431
+ expect(h.store.reported[0]).toMatchObject({ id: diskRecord.id, status: "closed", closedReason: "gc" });
432
+ });
433
+
434
+ it("giveUp 执行时 record 已被 archive(内存移除 + 磁盘无 light)→ 双 miss 直接 return,零副作用", async () => {
435
+ const h = makeBinding({ sessionFile });
436
+ const rec = makeRecord();
437
+ h.store.memory.set("bg-1", rec);
438
+ // 视图检查(内存 running)之后、giveUp 磁盘兜底之前被 archive 的收尾竞争替身投影
439
+ let viewChecked = false;
440
+ h.store.store.getMutable = () => {
441
+ if (!viewChecked) {
442
+ viewChecked = true;
443
+ return rec;
444
+ }
445
+ return undefined;
446
+ };
447
+ const supervisor = createRoundSupervisorForService(h.binding);
448
+
449
+ await adoptThenExpire(supervisor, rec);
450
+
451
+ expect(h.finalizeClosed).not.toHaveBeenCalled();
452
+ expect(h.pi?.sent).toHaveLength(2); // 纳管期通知照常,终止通知不发(零通知放弃)
453
+ expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
454
+ expect(h.store.reported).toHaveLength(0);
455
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
456
+ });
457
+ });
458
+
459
+ describe("runPendingReconcileSweepForService 的 subagent 判据(lookupRecordState 闭包)", () => {
460
+ /** sweep harness:PI_CODING_AGENT_DIR stub(sweep 内 FileRunStore 读侧解析不触真实目录)。 */
461
+ function setup(): { agentDir: string } {
462
+ const agentDir = path.join(tmpDir, "agent");
463
+ vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
464
+ return { agentDir };
465
+ }
466
+
467
+ function writeRegister(id: string, type: string): void {
468
+ fs.writeFileSync(
469
+ sessionFile,
470
+ JSON.stringify({ customType: "pending:register", data: { id, type, name: id } }) + "\n",
471
+ "utf-8",
472
+ );
473
+ }
474
+
475
+ it("isChildProcess=true → 空跑(不读 store、不读 session 文件)", () => {
476
+ setup();
477
+ const h = makeBinding({ sessionFile });
478
+ const getMutableSpy = vi.spyOn(h.store.store, "getMutable");
479
+ runPendingReconcileSweepForService(h.binding, true);
480
+ expect(getMutableSpy).not.toHaveBeenCalled();
481
+ expect(h.pi?.appended ?? []).toHaveLength(0);
482
+ });
483
+
484
+ it("getMainSessionFile undefined → 空跑(差集输入源缺席,store 零读取)", () => {
485
+ setup();
486
+ const h = makeBinding({});
487
+ const getMutableSpy = vi.spyOn(h.store.store, "getMutable");
488
+ runPendingReconcileSweepForService(h.binding, false);
489
+ expect(getMutableSpy).not.toHaveBeenCalled();
490
+ expect(h.pi?.appended ?? []).toHaveLength(0);
491
+ });
492
+
493
+ it("活跃 subagent record(内存 running)→ 不补注销(误注销活跃 record 的回归钉)", () => {
494
+ setup();
495
+ writeRegister("bg-live", "subagent");
496
+ const h = makeBinding({ sessionFile });
497
+ h.store.memory.set("bg-live", makeRecord({ id: "bg-live" }));
498
+ runPendingReconcileSweepForService(h.binding, false);
499
+ expect(h.pi?.appended ?? []).toHaveLength(0);
500
+ expect(h.pi?.emitted ?? []).toHaveLength(0);
501
+ });
502
+
503
+ it("活跃 subagent record(仅磁盘 running,boot 重认领形态)→ 不补注销", () => {
504
+ setup();
505
+ writeRegister("bg-disk", "subagent");
506
+ const h = makeBinding({ sessionFile });
507
+ h.store.disk.set("bg-disk", makeDiskRecord(sessionFile, { id: "bg-disk" }));
508
+ runPendingReconcileSweepForService(h.binding, false);
509
+ expect(h.pi?.appended ?? []).toHaveLength(0);
510
+ });
511
+
512
+ it("终态 subagent record(内存 closed,closedReason 有值)→ 差集补发注销(reason 直传 closedReason)+ 尽力 emit", () => {
513
+ setup();
514
+ writeRegister("bg-done", "subagent");
515
+ const h = makeBinding({ sessionFile });
516
+ h.store.memory.set("bg-done", makeRecord({ id: "bg-done", status: "closed", closedReason: "cancelled" }));
517
+ runPendingReconcileSweepForService(h.binding, false);
518
+ expect(h.pi?.appended).toEqual([
519
+ { customType: "pending:unregister", data: { id: "bg-done", reason: "cancelled", status: "cancelled" } },
520
+ ]);
521
+ expect(h.pi?.emitted).toEqual([{ channel: "pending:unregister", data: { id: "bg-done", reason: "cancelled" } }]);
522
+ });
523
+
524
+ it("终态 subagent record(磁盘 closed,closedReason 缺失)→ 补发注销 reason=completed 兜底", () => {
525
+ setup();
526
+ writeRegister("bg-old", "subagent");
527
+ const h = makeBinding({ sessionFile });
528
+ h.store.disk.set(
529
+ "bg-old",
530
+ makeDiskRecord(sessionFile, { id: "bg-old", status: "closed", resumable: false }),
531
+ );
532
+ runPendingReconcileSweepForService(h.binding, false);
533
+ expect(h.pi?.appended).toEqual([
534
+ { customType: "pending:unregister", data: { id: "bg-old", reason: "completed", status: "completed" } },
535
+ ]);
536
+ });
537
+
538
+ it("record 已归档/不存在(双 miss)→ 视同终态补注销(reason=expired)", () => {
539
+ setup();
540
+ writeRegister("bg-gone", "subagent");
541
+ const h = makeBinding({ sessionFile });
542
+ runPendingReconcileSweepForService(h.binding, false);
543
+ expect(h.pi?.appended).toEqual([
544
+ { customType: "pending:unregister", data: { id: "bg-gone", reason: "expired", status: "expired" } },
545
+ ]);
546
+ });
547
+
548
+ it("pi 缺席(getPi null)→ 只判不写,不炸", () => {
549
+ setup();
550
+ writeRegister("bg-gone", "subagent");
551
+ const h = makeBinding({ sessionFile, pi: null });
552
+ expect(() => runPendingReconcileSweepForService(h.binding, false)).not.toThrow();
553
+ });
554
+
555
+ it("getStore 抛错(store 未初始化形态)→ best-effort 吞掉不向上抛", () => {
556
+ setup();
557
+ // 差集非空是前置:无 register entry 时 collectActiveRegisterEntries 返回空、
558
+ // 循环零次、lookupRecordState(内含 getStore)不可达,用例空转(MF-R2-2)。
559
+ writeRegister("bg-gone", "subagent");
560
+ const h = makeBinding({ sessionFile });
561
+ let storeRequested = 0;
562
+ h.binding.getStore = () => {
563
+ storeRequested += 1;
564
+ throw new Error("store exploded");
565
+ };
566
+ expect(() => runPendingReconcileSweepForService(h.binding, false)).not.toThrow();
567
+ // 空转守卫:getStore 未被触达 = 判据链路没跑,异常根本没进 sweep 外层 catch
568
+ expect(storeRequested).toBeGreaterThanOrEqual(1);
569
+ });
570
+ });
571
+
572
+ describe("createRoundSupervisorForService 通知装配", () => {
573
+ it("merged failure notice:死亡纳管单条通知(failed 如实 + 已接管契约)经 steer", () => {
574
+ const h = makeBinding({ sessionFile });
575
+ h.store.memory.set("bg-1", makeRecord());
576
+ const supervisor = createRoundSupervisorForService(h.binding);
577
+
578
+ supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
579
+
580
+ const first = h.pi?.sent[0]!;
581
+ expect(first.options).toEqual({ deliverAs: "steer" });
582
+ expect(first.message.customType).toBe("subagent-round-supervisor");
583
+ expect(first.message.display).toBe(false);
584
+ expect(first.message.content).toContain('Subagent "worker" (fix-bug)');
585
+ expect(first.message.content).toContain("failed: the engine/child process driving it died (engine crashed)");
586
+ expect(first.message.content).toContain("NOT completed");
587
+ expect(first.message.content).toContain("Do NOT re-dispatch this task before that guidance arrives");
588
+ });
589
+
590
+ it("decision guidance:无替代命中 → 指引不带豁免声明;slug 缺失回落 record id", () => {
591
+ const h = makeBinding({ sessionFile });
592
+ h.store.memory.set("bg-1", makeRecord({ slug: "" }));
593
+ const supervisor = createRoundSupervisorForService(h.binding);
594
+
595
+ supervisor.adoptOnProcessDeath(makeRecord({ slug: "" }), "engine crashed");
596
+
597
+ const guidance = h.pi?.sent[1]!;
598
+ expect(guidance.message.content).toContain('Subagent "worker" (bg-1, id=bg-1) died mid-task');
599
+ expect(guidance.message.content).toContain("Decide now");
600
+ expect(guidance.message.content).toContain("it will be terminated automatically when the decision watchdog expires");
601
+ expect(guidance.message.content).not.toContain("If you have already re-dispatched");
602
+ });
603
+
604
+ it("低置信对账(仅 agent 同名)→ 指引自带豁免声明文案", () => {
605
+ const h = makeBinding({ sessionFile });
606
+ h.store.memory.set("bg-1", makeRecord());
607
+ h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "other-task", startedAt: Date.now() - 1000 });
608
+ const supervisor = createRoundSupervisorForService(h.binding);
609
+
610
+ supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
611
+
612
+ const guidance = h.pi?.sent[1]!;
613
+ expect(guidance.message.content).toContain(
614
+ "If you have already re-dispatched or no longer need this task, ignore this guidance",
615
+ );
616
+ });
617
+
618
+ it("高置信替代 → sendReplacedNotice 文案(原任务终止、无需 resume)", () => {
619
+ const h = makeBinding({ sessionFile });
620
+ h.store.memory.set("bg-1", makeRecord());
621
+ h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
622
+ const supervisor = createRoundSupervisorForService(h.binding);
623
+
624
+ supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
625
+
626
+ const notice = h.pi?.sent[1]!;
627
+ expect(notice.message.content).toContain("was already replaced by your new task bg-2");
628
+ expect(notice.message.content).toContain("no resume is needed");
629
+ });
630
+
631
+ it("candidates 接线:collectRecords 以 (SCAN_LIMIT, running, rootId) 调用;rootId null → undefined filter", () => {
632
+ const h = makeBinding({ sessionFile, rootId: "root-1" });
633
+ h.store.collectResult.push({ id: "bg-1", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: 1 });
634
+ const supervisor = createRoundSupervisorForService(h.binding);
635
+ supervisor.bootPartition();
636
+ expect(h.store.collectCalls).toEqual([
637
+ { limit: COLD_LOOKUP_SCAN_LIMIT, status: "running", rootFilter: "root-1" },
638
+ ]);
639
+
640
+ const h2 = makeBinding({ sessionFile, rootId: null });
641
+ const supervisor2 = createRoundSupervisorForService(h2.binding);
642
+ supervisor2.bootPartition();
643
+ expect(h2.store.collectCalls).toEqual([
644
+ { limit: COLD_LOOKUP_SCAN_LIMIT, status: "running", rootFilter: undefined },
645
+ ]);
646
+ });
647
+
648
+ it("record 视图磁盘兜底投影:内存 miss 时 boot 分区经 findLightById 读视图并可重认领", () => {
649
+ const h = makeBinding({ sessionFile });
650
+ h.store.disk.set("bg-disk", makeDiskRecord(sessionFile));
651
+ h.store.collectResult.push({ id: "bg-disk", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: 1 });
652
+ const supervisor = createRoundSupervisorForService(h.binding);
653
+
654
+ const { readopted } = supervisor.bootPartition();
655
+
656
+ expect(readopted).toEqual(["bg-disk"]);
657
+ // 磁盘投影视图驱动判定:resumable + 无完成产出 → 决策指引送达
658
+ expect(h.pi?.sent).toHaveLength(1);
659
+ expect(h.pi?.sent[0]?.message.content).toContain("died mid-task and stays resumable");
660
+ });
661
+ });