@zhushanwen/subagent-core 0.2.0 → 0.5.1

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 (347) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-43ONBFZX.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-V3VHZ2VX.js +59 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +62 -9
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +116 -12
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +116 -12
  18. package/dist/execution/engine/engines/zcode/constants.js +39 -7
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +18234 -1277
  24. package/dist/index.d.cts +5646 -673
  25. package/dist/index.d.ts +5646 -673
  26. package/dist/index.js +17828 -1277
  27. package/dist/{types-BxyAidGf.d.cts → types-Dv4QhSJ_.d.cts} +678 -112
  28. package/dist/{types-BxyAidGf.d.ts → types-Dv4QhSJ_.d.ts} +678 -112
  29. package/package.json +7 -4
  30. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
  31. package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
  32. package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
  33. package/src/__tests__/m2-append-content-probe.test.ts +0 -1
  34. package/src/__tests__/manifest-store.test.ts +10 -9
  35. package/src/__tests__/record-store-cache.test.ts +0 -2
  36. package/src/__tests__/record-store-index.test.ts +1 -2
  37. package/src/__tests__/review-fix-loop-script.test.ts +93 -3
  38. package/src/__tests__/review-fix-loop-utils.test.ts +31 -10
  39. package/src/__tests__/robustness-low-batch1.test.ts +6 -5
  40. package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
  41. package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
  42. package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
  43. package/src/__tests__/session-runner.test.ts +1 -1
  44. package/src/__tests__/smoke.test.ts +9 -2
  45. package/src/core/__tests__/host-services.test.ts +2 -2
  46. package/src/core/__tests__/logger.test.ts +1 -1
  47. package/src/core/error-message.ts +9 -0
  48. package/src/core/host-services.ts +21 -5
  49. package/src/core/notify-ports.ts +18 -5
  50. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  51. package/src/execution/__tests__/agent-registry.test.ts +370 -0
  52. package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
  53. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  54. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
  55. package/src/execution/__tests__/chat-engine-routing.test.ts +136 -30
  56. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +5 -4
  57. package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
  58. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  59. package/src/execution/__tests__/delivery-methods.test.ts +77 -62
  60. package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
  61. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  62. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  63. package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
  64. package/src/execution/__tests__/epipe-fallback.test.ts +31 -24
  65. package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
  66. package/src/execution/__tests__/execute-nesting.test.ts +34 -188
  67. package/src/execution/__tests__/execution-record.test.ts +217 -60
  68. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  69. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
  70. package/src/execution/__tests__/finalize-record.test.ts +278 -16
  71. package/src/execution/__tests__/gc-timer.test.ts +57 -2
  72. package/src/execution/__tests__/get-record-for-action-restart.test.ts +14 -12
  73. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  74. package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
  75. package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
  76. package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
  77. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
  78. package/src/execution/__tests__/keep-alive-no-progress.test.ts +280 -0
  79. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  80. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
  83. package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
  84. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  85. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  86. package/src/execution/__tests__/model-resolver.test.ts +2 -2
  87. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
  88. package/src/execution/__tests__/nested-visibility.test.ts +2 -0
  89. package/src/execution/__tests__/notify-ledger.test.ts +1249 -0
  90. package/src/execution/__tests__/output-collector.test.ts +117 -6
  91. package/src/execution/__tests__/pi-invocation.test.ts +1 -1
  92. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  93. package/src/execution/__tests__/record-store.test.ts +89 -5
  94. package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
  95. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
  96. package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
  97. package/src/execution/__tests__/rpc-mode.test.ts +2 -2
  98. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +19 -9
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +5 -4
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +418 -26
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +3 -3
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
  105. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +16 -8
  106. package/src/execution/__tests__/service-kill-escalation.test.ts +91 -0
  107. package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
  108. package/src/execution/__tests__/session-file-gc.test.ts +35 -0
  109. package/src/execution/__tests__/session-pending.test.ts +471 -0
  110. package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
  111. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  112. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  113. package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
  114. package/src/execution/__tests__/session-runner-epipe.test.ts +4 -3
  115. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +227 -0
  116. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
  117. package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
  118. package/src/execution/__tests__/settled-watchdog.test.ts +524 -0
  119. package/src/execution/__tests__/spawn-args.test.ts +11 -10
  120. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
  121. package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
  122. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
  123. package/src/execution/__tests__/spawned-children.test.ts +155 -11
  124. package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
  125. package/src/execution/__tests__/stdin-writer.test.ts +6 -4
  126. package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
  127. package/src/execution/__tests__/subagent-actions-core.test.ts +1017 -0
  128. package/src/execution/__tests__/subagent-service-message-close.test.ts +99 -35
  129. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  130. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +271 -0
  131. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +33 -29
  132. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +341 -0
  133. package/src/execution/__tests__/subagent-service.test.ts +37 -30
  134. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
  135. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
  136. package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
  137. package/src/execution/__tests__/temp-prompt.test.ts +1 -1
  138. package/src/execution/__tests__/timeout-integration.test.ts +13 -12
  139. package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
  140. package/src/execution/__tests__/turn-limiter.test.ts +1 -1
  141. package/src/execution/__tests__/ui-request-handler-factory.test.ts +107 -7
  142. package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
  143. package/src/execution/__tests__/ui-request-queue.test.ts +167 -2
  144. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  145. package/src/execution/__tests__/worktree-manager.test.ts +14 -5
  146. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
  147. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  148. package/src/execution/agent-registry.ts +298 -30
  149. package/src/execution/agent-result-mapper.ts +3 -0
  150. package/src/execution/agents-assembly.ts +88 -0
  151. package/src/execution/cold-resurrect.ts +199 -0
  152. package/src/execution/concurrency-pool.ts +71 -9
  153. package/src/execution/dialog-queue.ts +101 -3
  154. package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
  155. package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
  156. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
  158. package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
  159. package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
  160. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
  161. package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
  162. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +56 -99
  163. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +88 -15
  164. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +33 -7
  165. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
  166. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +75 -54
  167. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +128 -0
  168. package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
  169. package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
  170. package/src/execution/engine/__tests__/registry.test.ts +94 -3
  171. package/src/execution/engine/__tests__/routing.test.ts +115 -6
  172. package/src/execution/engine/common/capability-gate.ts +92 -0
  173. package/src/execution/engine/common/journal-wiring.ts +97 -0
  174. package/src/execution/engine/common/kill-chain.ts +42 -10
  175. package/src/execution/engine/common/nesting-guard.ts +66 -0
  176. package/src/execution/engine/common/persona-router.ts +18 -8
  177. package/src/execution/engine/common/pool-manager.ts +332 -78
  178. package/src/execution/engine/common/session-view-service.ts +506 -0
  179. package/src/execution/engine/common/session-view-types.ts +125 -0
  180. package/src/execution/engine/engine-discovery.ts +12 -16
  181. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
  182. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
  183. package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
  184. package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +86 -11
  185. package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
  186. package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
  187. package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
  188. package/src/execution/engine/engines/pi/reader.ts +11 -0
  189. package/src/execution/engine/engines/pi/registration.ts +4 -2
  190. package/src/execution/engine/engines/pi/session-runner.ts +2840 -0
  191. package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
  192. package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
  193. package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
  194. package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
  195. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +301 -0
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  197. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
  198. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +473 -0
  199. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
  200. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
  201. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
  202. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
  203. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +792 -0
  204. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +782 -0
  205. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +168 -0
  206. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
  207. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
  208. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
  209. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
  210. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +65 -47
  211. package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
  212. package/src/execution/engine/engines/zcode/connection.ts +598 -0
  213. package/src/execution/engine/engines/zcode/constants.ts +147 -14
  214. package/src/execution/engine/engines/zcode/golden-sample.ts +42 -34
  215. package/src/execution/engine/engines/zcode/parser.ts +12 -346
  216. package/src/execution/engine/engines/zcode/preparer.ts +22 -150
  217. package/src/execution/engine/engines/zcode/session-channel.ts +968 -0
  218. package/src/execution/engine/engines/zcode/zcode-engine.ts +1023 -250
  219. package/src/execution/engine/host-task-spec.ts +32 -36
  220. package/src/execution/engine/model-validation.ts +177 -0
  221. package/src/execution/engine/port.ts +65 -7
  222. package/src/execution/engine/registry.ts +53 -0
  223. package/src/execution/engine/routing.ts +75 -1
  224. package/src/execution/engine/types.ts +30 -103
  225. package/src/execution/execution-record.ts +222 -144
  226. package/src/execution/finalize-record.ts +181 -85
  227. package/src/execution/idle-gc.ts +28 -0
  228. package/src/execution/lifecycle-manager.ts +27 -3
  229. package/src/execution/lifecycle-predicates.ts +1 -1
  230. package/src/execution/manifest-store.ts +53 -62
  231. package/src/execution/notifier.ts +10 -10
  232. package/src/execution/notify-host.ts +212 -0
  233. package/src/execution/notify-ledger.ts +117 -16
  234. package/src/execution/record-entry.ts +8 -2
  235. package/src/execution/record-store.ts +148 -88
  236. package/src/execution/round-settlement.ts +93 -0
  237. package/src/execution/session-file-gc.ts +96 -62
  238. package/src/execution/session-pending.ts +213 -62
  239. package/src/execution/session-reconstructor.ts +256 -143
  240. package/src/execution/sessions-index.ts +127 -102
  241. package/src/execution/settled-watchdog.ts +311 -0
  242. package/src/execution/subagent-actions-core.ts +686 -0
  243. package/src/execution/subagent-service.ts +1106 -907
  244. package/src/execution/subprocess-agent-runner.ts +144 -97
  245. package/src/execution/types.ts +9 -0
  246. package/src/execution/ui-request-handler-factory.ts +126 -75
  247. package/src/execution/ui-request-queue.ts +50 -19
  248. package/src/execution/worktree-git-ops.ts +397 -0
  249. package/src/execution/worktree-manager.ts +72 -10
  250. package/src/execution/worktree-registry.ts +6 -12
  251. package/src/index.ts +542 -9
  252. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  253. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +3 -5
  254. package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
  255. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +14 -0
  257. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  258. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +384 -0
  259. package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
  260. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  261. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  262. package/src/orchestration/__tests__/file-run-store.test.ts +390 -0
  263. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  264. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  265. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  266. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  267. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
  268. package/src/orchestration/__tests__/lifecycle.test.ts +84 -5
  269. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
  270. package/src/orchestration/__tests__/run-snapshot.test.ts +356 -0
  271. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  272. package/src/orchestration/__tests__/script-lint.test.ts +17 -0
  273. package/src/orchestration/__tests__/test-mocks.ts +2 -0
  274. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
  275. package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
  276. package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +28 -27
  277. package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
  278. package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
  279. package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
  280. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  281. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  282. package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
  283. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  284. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
  285. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  286. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  287. package/src/orchestration/agent-opts-resolver.ts +8 -8
  288. package/src/orchestration/args-meta.ts +198 -0
  289. package/src/orchestration/args-validator.ts +53 -31
  290. package/src/orchestration/execute-agent-call.ts +25 -79
  291. package/src/orchestration/file-run-store.ts +327 -0
  292. package/src/orchestration/launcher.ts +160 -104
  293. package/src/orchestration/lifecycle.ts +371 -97
  294. package/src/orchestration/models/agent-call.ts +7 -7
  295. package/src/orchestration/models/budget.ts +5 -5
  296. package/src/orchestration/models/ports.ts +4 -4
  297. package/src/orchestration/models/run-runtime.ts +15 -15
  298. package/src/orchestration/models/trace.ts +9 -9
  299. package/src/orchestration/models/types.ts +111 -22
  300. package/src/orchestration/models/workflow-run.ts +28 -28
  301. package/src/orchestration/models/workflow-script.ts +4 -4
  302. package/src/orchestration/run-snapshot.ts +266 -0
  303. package/src/orchestration/script-generate.ts +154 -0
  304. package/src/orchestration/script-lint.ts +123 -90
  305. package/src/orchestration/worker-handle.ts +10 -10
  306. package/src/orchestration/worker-host.ts +10 -10
  307. package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +498 -221
  308. package/src/orchestration/worker-script-builder.ts +401 -346
  309. package/src/orchestration/workflow-files.ts +37 -11
  310. package/src/orchestration/workflow-run-summary.ts +69 -0
  311. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  312. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  313. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  314. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  315. package/src/shared/__tests__/injection-render.test.ts +518 -0
  316. package/src/shared/__tests__/meta-parser.test.ts +151 -0
  317. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  318. package/src/shared/__tests__/resource-discovery.test.ts +261 -217
  319. package/src/shared/agent-ref.ts +143 -2
  320. package/src/shared/atomic-write.ts +323 -0
  321. package/src/shared/bounded-serialize.ts +154 -0
  322. package/src/shared/injection-render.ts +279 -0
  323. package/src/shared/meta-parser.ts +182 -68
  324. package/src/shared/model-ref.ts +15 -2
  325. package/src/shared/resource-discovery.ts +97 -204
  326. package/src/shared/resource-meta.ts +14 -0
  327. package/src/shared/schema-jsonify.ts +1 -1
  328. package/src/shared/xml-injection.ts +9 -9
  329. package/workflows/README.md +9 -9
  330. package/workflows/chain.js +4 -2
  331. package/workflows/map-reduce.js +5 -3
  332. package/workflows/parallel.js +5 -3
  333. package/workflows/review-fix-loop-utils.cjs +165 -92
  334. package/workflows/review-fix-loop-utils.d.cts +287 -0
  335. package/workflows/review-fix-loop.js +18 -7
  336. package/workflows/scatter-gather.js +4 -2
  337. package/dist/chunk-3VOERJPJ.js +0 -22
  338. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
  339. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  340. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  341. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  342. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  343. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
  344. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  345. package/src/execution/execute-options-mapper.ts +0 -115
  346. package/src/execution/session-runner.ts +0 -1789
  347. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
@@ -13,25 +13,33 @@ import type { ChildProcess } from "node:child_process";
13
13
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
14
 
15
15
  // mock logger(doFinalizeRecord manifest 写入降级路径用)
16
- const { loggerMock } = vi.hoisted(() => ({
16
+ const { loggerMock, killEscalationSpy } = vi.hoisted(() => ({
17
17
  loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
18
+ killEscalationSpy: vi.fn(),
18
19
  }));
19
20
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
20
21
 
21
22
  // mock session-runner(import 链需要 runSpawn/killAllSpawnedChildren/getChildByRecord 存在)
22
- vi.mock("../session-runner.ts", () => ({
23
+ vi.mock("../engine/engines/pi/session-runner.ts", () => ({
23
24
  runSpawn: vi.fn(),
24
25
  killAllSpawnedChildren: vi.fn(),
25
26
  getChildByRecord: vi.fn(() => undefined),
27
+ registerSpawnedChildForRecord: vi.fn(),
28
+ killRecordChildWithEscalation: killEscalationSpy,
29
+ spawnedChildren: new Map(),
26
30
  }));
27
31
 
28
32
  import { createRecord, updateFromEvent } from "../execution-record.ts";
33
+ import { createNotifyHost } from "../notify-host.ts";
29
34
  import { ModelConfigService } from "../model-config-service.ts";
30
35
  import { RecordStore } from "../record-store.ts";
31
36
  import { SubagentService } from "../subagent-service.ts";
32
37
  import type { PiLike } from "../subagent-service.ts";
33
38
  import type { ExecutionRecord } from "../types.ts";
34
- import { getChildByRecord } from "../session-runner.ts";
39
+ import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
40
+
41
+ // [T2④] kill 收敛入口 spy(工厂引用,断言服务侧三条路径收敛到它)
42
+ const killRecordChildWithEscalationMock = killEscalationSpy;
35
43
  import {
36
44
  armIdleTimer,
37
45
  disarmIdleTimer,
@@ -88,7 +96,7 @@ function setup(): {
88
96
  pi: MockPi;
89
97
  } {
90
98
  const agentDir = makeTmpAgentDir();
91
- const modelService = new ModelConfigService({ agentDir });
99
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
92
100
  const service = new SubagentService({ cwd: agentDir, modelService });
93
101
  // [C2] pi 提为外层引用(对照 real-chain :139-140 形态)——close 现状语义断言需读
94
102
  // pi.sendMessage.mock.calls(末条通知 content 指针行),内联构造不保留引用。
@@ -114,23 +122,23 @@ describe("getRecordForAction 归属守卫(决策 3)", () => {
114
122
 
115
123
  afterEach(() => {
116
124
  service.dispose();
117
- fs.rmSync(agentDir, { recursive: true, force: true });
125
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
118
126
  });
119
127
 
120
128
  it("正常(rootSessionId 匹配)→ 返回可变 record", () => {
121
129
  const record = makeRecord({ rootSessionId: sessionRootId });
122
130
  store.register(record);
123
- expect(service.getRecordForAction(record.id)).toBe(record);
131
+ expect(service.chatActions.getRecordForAction(record.id)).toBe(record);
124
132
  });
125
133
 
126
134
  it("不存在 → throw not found or not owned", () => {
127
- expect(() => service.getRecordForAction("sa-nonexistent")).toThrow(/not found or not owned/);
135
+ expect(() => service.chatActions.getRecordForAction("sa-nonexistent")).toThrow(/not found or not owned/);
128
136
  });
129
137
 
130
138
  it("rootSessionId 不匹配 → throw not found or not owned(不区分 not found vs not owned,防信息泄露)", () => {
131
139
  const record = makeRecord({ id: "sa-other", rootSessionId: "other-session" });
132
140
  store.register(record);
133
- expect(() => service.getRecordForAction("sa-other")).toThrow(/not found or not owned/);
141
+ expect(() => service.chatActions.getRecordForAction("sa-other")).toThrow(/not found or not owned/);
134
142
  });
135
143
  });
136
144
 
@@ -150,7 +158,7 @@ describe("closeSubagent 行为分流", () => {
150
158
 
151
159
  afterEach(() => {
152
160
  service.dispose();
153
- fs.rmSync(agentDir, { recursive: true, force: true });
161
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
154
162
  });
155
163
 
156
164
  it("running + force:false + 有活进程 → 置 closeAfterRound=true(不立即终态,等轮完)", async () => {
@@ -160,7 +168,7 @@ describe("closeSubagent 行为分流", () => {
160
168
  const record = makeRecord({ status: "running" });
161
169
  store.register(record);
162
170
 
163
- await service.closeSubagent(record, false);
171
+ await service.chatActions.closeSubagent(record, false);
164
172
 
165
173
  expect(record.closeAfterRound).toBe(true);
166
174
  expect(record.status).toBe("running"); // 仍 running
@@ -174,7 +182,7 @@ describe("closeSubagent 行为分流", () => {
174
182
  record.sessionFile = path.join(agentDir, "test.jsonl");
175
183
  store.register(record);
176
184
 
177
- await service.closeSubagent(record, false);
185
+ await service.chatActions.closeSubagent(record, false);
178
186
 
179
187
  expect(record.status).toBe("closed");
180
188
  expect(store.getMutable(record.id)).toBeUndefined(); // archived
@@ -184,7 +192,7 @@ describe("closeSubagent 行为分流", () => {
184
192
  const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "run.jsonl") });
185
193
  store.register(record);
186
194
 
187
- await service.closeSubagent(record, true);
195
+ await service.chatActions.closeSubagent(record, true);
188
196
 
189
197
  // v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消语义)
190
198
  expect(record.status).toBe("closed");
@@ -196,7 +204,7 @@ describe("closeSubagent 行为分流", () => {
196
204
  const record = makeRecord({ status: "closed" });
197
205
  store.register(record);
198
206
 
199
- await service.closeSubagent(record, false);
207
+ await service.chatActions.closeSubagent(record, false);
200
208
 
201
209
  expect(record.status).toBe("closed"); // 不变
202
210
  });
@@ -207,9 +215,8 @@ describe("closeSubagent 行为分流", () => {
207
215
  // 旧代码 Path A 走 else 置 closeAfterRound,但 chatMode 轮完成恒经 agent_settled →
208
216
  // onRoundSettled → runAndFinalize early return(isIdle 恒 true),标志无人消费,
209
217
  // tool 却返回 {closed:true}(谎报)。
210
- const killFn = vi.fn(() => true);
211
- const child = { killed: false, kill: killFn } as unknown as ChildProcess;
212
- vi.mocked(getChildByRecord).mockReturnValueOnce(child);
218
+ // [T2④] closeChatIdle kill 收敛到 killRecordChildWithEscalation(recordId 入口),
219
+ // 不再读 child 句柄——无 child mock(Once mock 不被消费会泄漏劫持后续用例)
213
220
  const record = makeRecord({ status: "running", round: 1 });
214
221
  record.sessionFile = path.join(agentDir, "path-a.jsonl");
215
222
  store.register(record);
@@ -217,7 +224,7 @@ describe("closeSubagent 行为分流", () => {
217
224
  expect(hasIdleTimer(record.id)).toBe(true);
218
225
 
219
226
  try {
220
- await service.closeSubagent(record, false);
227
+ await service.chatActions.closeSubagent(record, false);
221
228
  } finally {
222
229
  disarmIdleTimer(record.id); // 断言失败路径的兜底清理
223
230
  }
@@ -226,7 +233,8 @@ describe("closeSubagent 行为分流", () => {
226
233
  expect(record.status).toBe("closed");
227
234
  expect(record.closedReason).toBe("user-close");
228
235
  expect(store.getMutable(record.id)).toBeUndefined(); // archived
229
- expect(killFn).toHaveBeenCalledWith("SIGTERM"); // 保活进程回收
236
+ // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 30s SIGKILL 升级)
237
+ expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeChatIdle");
230
238
  expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
231
239
  });
232
240
 
@@ -235,7 +243,7 @@ describe("closeSubagent 行为分流", () => {
235
243
  record.sessionFile = path.join(agentDir, "path-b.jsonl");
236
244
  store.register(record);
237
245
 
238
- await service.closeSubagent(record, false);
246
+ await service.chatActions.closeSubagent(record, false);
239
247
 
240
248
  expect(record.status).toBe("closed");
241
249
  expect(record.closedReason).toBe("user-close");
@@ -253,9 +261,8 @@ describe("closeSubagent 行为分流", () => {
253
261
  it("[M6] running + force:true + 活进程(热路径轮中)→ cancelBackground 显式 SIGTERM + disarm idle timer", async () => {
254
262
  // chatMode 首轮 agent_settled 后 runSpawn 提前 resolveRun(0) 返回,
255
263
  // abort→kill listener 已被 removeEventListener——热路径轮中 cancel 只能靠显式 kill。
256
- const killFn = vi.fn(() => true);
257
- const child = { killed: false, kill: killFn } as unknown as ChildProcess;
258
- vi.mocked(getChildByRecord).mockReturnValueOnce(child);
264
+ // [T2④] cancelBackground kill 收敛到 killRecordChildWithEscalation(recordId 入口),
265
+ // 不再读 child 句柄
259
266
  const record = makeRecord({ status: "running", sessionFile: path.join(agentDir, "hot.jsonl") });
260
267
  record.controller = new AbortController();
261
268
  store.register(record);
@@ -263,12 +270,13 @@ describe("closeSubagent 行为分流", () => {
263
270
  expect(hasIdleTimer(record.id)).toBe(true);
264
271
 
265
272
  try {
266
- await service.closeSubagent(record, true);
273
+ await service.chatActions.closeSubagent(record, true);
267
274
  } finally {
268
275
  disarmIdleTimer(record.id); // 断言失败路径的兜底清理
269
276
  }
270
277
 
271
- expect(killFn).toHaveBeenCalledWith("SIGTERM"); // 子进程收到 kill
278
+ // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
279
+ expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "cancelBackground");
272
280
  expect(record.status).toBe("closed");
273
281
  expect(record.closedReason).toBe("cancelled");
274
282
  expect(hasIdleTimer(record.id)).toBe(false); // timer disarmed
@@ -276,6 +284,59 @@ describe("closeSubagent 行为分流", () => {
276
284
  });
277
285
  });
278
286
 
287
+ // ============================================================
288
+ // [A2-1] cancel 迟到:record 已终态(detached 已 finalize)→ CAS 拒绝
289
+ // ============================================================
290
+
291
+ describe("cancelBackground CAS 抢锁(A2-1)——cancel 迟到不覆写已完成 record", () => {
292
+ let agentDir: string;
293
+ let service: SubagentService;
294
+ let store: RecordStore;
295
+ let pi: MockPi;
296
+
297
+ beforeEach(() => {
298
+ ({ agentDir, service, store, pi } = setup());
299
+ });
300
+
301
+ afterEach(() => {
302
+ service.dispose();
303
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
304
+ });
305
+
306
+ it("record 已 closed 未 archive(doFinalizeRecord Step 0 await 窗口形态)→ cancel 返回 false,不覆写终态/不写 tombstone/不 notify/不 archive", () => {
307
+ const sessionFile = path.join(agentDir, "late-cancel.jsonl");
308
+ const record = makeRecord({ status: "closed", sessionFile });
309
+ // 模拟 detached 已 finalize 的终态快照(closedReason=gc 正常完成,非 cancelled)
310
+ record.closedReason = "gc";
311
+ record.endedAt = 5555;
312
+ record.result = "detached final result";
313
+ store.register(record); // 未 archive:恰好处于 finalize await 窗口(收尾后段才 archive)
314
+
315
+ const cancelled = service.cancel(record.id);
316
+
317
+ expect(cancelled).toBe(false); // CAS 没抢到——不谎报 true
318
+ expect(record.status).toBe("closed");
319
+ expect(record.closedReason).toBe("gc"); // 不被 completeRecord 改写为 cancelled
320
+ expect(record.endedAt).toBe(5555); // finalize 冻结的终态值不被重写
321
+ expect(record.result).toBe("detached final result"); // 不被空 cancelled result 覆写
322
+ // 不写 cancelled tombstone(与 finalized marker 双标 = record 双重终态化)
323
+ expect(fs.existsSync(`${sessionFile}.cancelled`)).toBe(false);
324
+ expect(store.getMutable(record.id)).toBe(record); // 不 archive(record 留内存)
325
+ expect(pi.sendMessage).not.toHaveBeenCalled(); // 不重复 notify(detached finalize 已发)
326
+ });
327
+
328
+ it("record running → cancel 返回 true 且终态化(CAS 正常路径不回归)", () => {
329
+ const sessionFile = path.join(agentDir, "cancel-ok.jsonl");
330
+ const record = makeRecord({ status: "running", sessionFile });
331
+ store.register(record);
332
+
333
+ expect(service.cancel(record.id)).toBe(true);
334
+ expect(record.status).toBe("closed");
335
+ expect(record.closedReason).toBe("cancelled");
336
+ expect(store.getMutable(record.id)).toBeUndefined(); // archived
337
+ });
338
+ });
339
+
279
340
  // ============================================================
280
341
  // [M5] closeAfterRound 消费点:onRoundSettled(chatMode 热路径轮完成)
281
342
  // ============================================================
@@ -294,7 +355,7 @@ describe("[M5] closeAfterRound 消费点挂 onRoundSettled(chatMode 轮完成
294
355
  afterEach(() => {
295
356
  service.dispose();
296
357
  _resetLifecycleState();
297
- fs.rmSync(agentDir, { recursive: true, force: true });
358
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
298
359
  });
299
360
 
300
361
  it("热路径轮完成(onRoundSettled)+ closeAfterRound=true → 消费标志终态化(closed+user-close + archive)", async () => {
@@ -325,7 +386,8 @@ describe("[M5] closeAfterRound 消费点挂 onRoundSettled(chatMode 轮完成
325
386
  expect(record.closeAfterRound).toBeUndefined(); // 标志已消费
326
387
  expect(record.status).toBe("closed");
327
388
  expect(record.closedReason).toBe("user-close");
328
- expect(killFn).toHaveBeenCalledWith("SIGTERM"); // 保活进程回收
389
+ // [T2④] kill 收敛到 killRecordChildWithEscalation(SIGTERM + 升级)
390
+ expect(killRecordChildWithEscalationMock).toHaveBeenCalledWith(record.id, "closeAfterRoundSettled");
329
391
  expect(hasIdleTimer(record.id)).toBe(false);
330
392
  });
331
393
 
@@ -367,7 +429,7 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
367
429
  afterEach(() => {
368
430
  service.dispose();
369
431
  _resetLifecycleState();
370
- fs.rmSync(agentDir, { recursive: true, force: true });
432
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
371
433
  });
372
434
 
373
435
  it("close 语义:轮次通知(含指针行)发出后 closeAfterRoundSettled 终态化 + 终态通知,两条都送达", async () => {
@@ -412,8 +474,10 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
412
474
 
413
475
  // running + 无活进程(getChildByRecord mock undefined)→ isResumable 放行 →
414
476
  // 非 chatMode 映射 closed 通知,立即 flush(无其他 running background)
415
- const svc = service as unknown as { notifyComplete(r: ExecutionRecord): void };
416
- svc.notifyComplete(record);
477
+ // [D4-①] notifyComplete 已随通知簇搬至 notify-host——按轴直接构造(同 pi/store
478
+ // 注入,行为与经 Service 的原路径等价;dedup 集为空与单次调用断言相容)
479
+ createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
480
+ .notifyComplete(record);
417
481
 
418
482
  expect(pi.sendMessage).toHaveBeenCalledTimes(1);
419
483
  const msg = pi.sendMessage.mock.calls[0]![0] as {
@@ -432,8 +496,8 @@ describe("[C2] close 现状语义 + sessionFile 条件透传", () => {
432
496
  record.sessionFile = path.join(agentDir, "c2-chat.jsonl");
433
497
  store.register(record);
434
498
 
435
- const svc = service as unknown as { notifyComplete(r: ExecutionRecord): void };
436
- svc.notifyComplete(record);
499
+ createNotifyHost({ getPi: () => pi, listRunning: () => store.listRunning(), getIsIdle: () => undefined })
500
+ .notifyComplete(record);
437
501
 
438
502
  expect(pi.sendMessage).toHaveBeenCalledTimes(1);
439
503
  const msg = pi.sendMessage.mock.calls[0]![0] as {
@@ -465,7 +529,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
465
529
  afterEach(() => {
466
530
  service.dispose();
467
531
  _resetLifecycleState();
468
- fs.rmSync(agentDir, { recursive: true, force: true });
532
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
469
533
  });
470
534
 
471
535
  /** 真实回调链(与生产一致:agent_settled → onRoundSettled),turns 经 updateFromEvent 累积。 */
@@ -540,7 +604,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
540
604
  updateFromEvent(record, { type: "text_delta", delta: "LAST-ROUND-INCREMENT" });
541
605
  updateFromEvent(record, { type: "turn_end" });
542
606
  settleRound(record); // 第 1 条:轮次通知
543
- await service.closeSubagent(record, false); // idle close(D2 路径②)
607
+ await service.chatActions.closeSubagent(record, false); // idle close(D2 路径②)
544
608
  } finally {
545
609
  disarmIdleTimer(record.id);
546
610
  }
@@ -590,7 +654,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
590
654
  record.sessionFile = path.join(agentDir, "oneshot-close.jsonl");
591
655
  store.register(record);
592
656
 
593
- await service.closeSubagent(record, false); // resumable → closeChatIdle → notifyClosed 拒绝
657
+ await service.chatActions.closeSubagent(record, false); // resumable → closeChatIdle → notifyClosed 拒绝
594
658
 
595
659
  expect(record.status).toBe("closed");
596
660
  expect(pi.sendMessage).not.toHaveBeenCalled();
@@ -611,7 +675,7 @@ describe("[C-4] onRoundSettled 轮次增量 + [C-1] close 终态通知", () => {
611
675
  updateFromEvent(record, { type: "turn_end" });
612
676
  settleRound(record); // 轮终:onRoundSettled 写 record.result = "ok"
613
677
  expect(record.result).toBe("ok"); // 前提锚点:轮终真实值在位
614
- await service.closeSubagent(record, false); // idle close → completeRecord + archive
678
+ await service.chatActions.closeSubagent(record, false); // idle close → completeRecord + archive
615
679
  } finally {
616
680
  disarmIdleTimer(record.id);
617
681
  }
@@ -0,0 +1,174 @@
1
+ // src/execution/__tests__/subagent-service-multiproc-guard.test.ts
2
+ //
3
+ // [u-svc / T5] 多进程共享文件守卫(subagent-service 侧两项):
4
+ // - T5①/PS-8:子进程 initSession 跳过 recoverOrphanRecords(env PI_SUBAGENT_SELF_RECORD_ID
5
+ // 判子进程身份)——恢复机制假设「单扫描者」,env 贯穿让每个子进程都成了扫描者,
6
+ // 兄弟记录 marker 缺失/超软超时被无关进程盖终态 sidecar;
7
+ // - T5③/PS-7b:running 候选冷查补 findForeignLiveInstance 探针守卫——异进程活实例在册
8
+ // 时拒绝 resurrect + resume spawn(防双写者窗口)。
9
+ //
10
+ // mock 形态沿用 subagent-service-message-close.test.ts;alive-store mock(findForeignLiveInstance
11
+ // 换 spy 控制探针命中)。
12
+
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+
19
+ const { loggerMock } = vi.hoisted(() => ({
20
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
21
+ }));
22
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
23
+
24
+ const { killChildSpy, foreignLiveSpy } = vi.hoisted(() => ({
25
+ killChildSpy: vi.fn(),
26
+ foreignLiveSpy: vi.fn<() => { pid: number; id: string; startedAt: number } | undefined>(),
27
+ }));
28
+ vi.mock("../alive-store.ts", () => ({
29
+ writeAliveMarker: vi.fn(),
30
+ removeAliveMarker: vi.fn(),
31
+ readAliveMarker: vi.fn(() => undefined),
32
+ isProcessAlive: vi.fn(() => false),
33
+ ALIVE_SOFT_TIMEOUT_MS: 3_600_000,
34
+ findForeignLiveInstance: foreignLiveSpy,
35
+ }));
36
+
37
+ vi.mock("../engine/engines/pi/session-runner.ts", () => ({
38
+ runSpawn: vi.fn(),
39
+ killAllSpawnedChildren: vi.fn(),
40
+ getChildByRecord: vi.fn(() => undefined),
41
+ registerSpawnedChildForRecord: vi.fn(),
42
+ spawnedChildren: new Map(),
43
+ killRecordChildWithEscalation: killChildSpy,
44
+ }));
45
+
46
+ import { ModelConfigService } from "../model-config-service.ts";
47
+ import type { RecordStore } from "../record-store.ts";
48
+ import { SubagentService } from "../subagent-service.ts";
49
+ import { ResurrectDeniedError } from "../types.ts";
50
+ import type { PiLike } from "../subagent-service.ts";
51
+
52
+ function makeTmpAgentDir(): string {
53
+ return fs.mkdtempSync(path.join(os.tmpdir(), "svc-multiproc-"));
54
+ }
55
+
56
+ function makePi(): PiLike {
57
+ return {
58
+ appendEntry: vi.fn(),
59
+ events: { emit: vi.fn() },
60
+ sendMessage: vi.fn(),
61
+ } as unknown as PiLike;
62
+ }
63
+
64
+ function setup(): { agentDir: string; service: SubagentService; store: RecordStore } {
65
+ const agentDir = makeTmpAgentDir();
66
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
67
+ const service = new SubagentService({ cwd: agentDir, modelService });
68
+ service.initSession({ pi: makePi(), sessionId: "root-session" });
69
+ const store = (service as unknown as { store: RecordStore }).store;
70
+ return { agentDir, service, store };
71
+ }
72
+
73
+ describe("T5① child process skips orphan recovery scan", () => {
74
+ let agentDir: string;
75
+
76
+ /** 构造 service 并在其 store 上挂 spy 后再 initSession(spy 必须先于触发点)。 */
77
+ const makeServiceWithRecoverySpies = (): {
78
+ service: SubagentService;
79
+ recover: ReturnType<typeof vi.spyOn>;
80
+ entryOnly: ReturnType<typeof vi.spyOn>;
81
+ } => {
82
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
83
+ const svc = new SubagentService({ cwd: agentDir, modelService });
84
+ const store = (svc as unknown as { store: RecordStore }).store;
85
+ const recover = vi.spyOn(store, "recoverOrphanRecords").mockImplementation(() => {});
86
+ const entryOnly = vi.spyOn(store, "recoverEntryOnlyOrphans").mockImplementation(() => {});
87
+ return { service: svc, recover, entryOnly };
88
+ };
89
+
90
+ beforeEach(() => {
91
+ vi.stubEnv("PI_SUBAGENT_SELF_RECORD_ID", "");
92
+ agentDir = makeTmpAgentDir();
93
+ });
94
+
95
+ afterEach(() => {
96
+ vi.unstubAllEnvs();
97
+ vi.restoreAllMocks();
98
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
99
+ });
100
+
101
+ it("root process (no self-record env) runs the orphan recovery scan", () => {
102
+ const { service: root, recover, entryOnly } = makeServiceWithRecoverySpies();
103
+ root.initSession({ pi: makePi(), sessionId: "root-session-2" });
104
+ expect(recover).toHaveBeenCalledTimes(1);
105
+ expect(entryOnly).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ it("child process (PI_SUBAGENT_SELF_RECORD_ID set) skips the scan entirely", () => {
109
+ vi.stubEnv("PI_SUBAGENT_SELF_RECORD_ID", "sa-child-self");
110
+ const { service: child, recover, entryOnly } = makeServiceWithRecoverySpies();
111
+ child.initSession({ pi: makePi(), sessionId: "child-session" });
112
+ expect(recover).not.toHaveBeenCalled();
113
+ expect(entryOnly).not.toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe("T5③ cold-lookup running candidate foreign-instance guard", () => {
118
+ let agentDir: string;
119
+ let service: SubagentService;
120
+ let store: RecordStore;
121
+
122
+ beforeEach(() => {
123
+ foreignLiveSpy.mockReset();
124
+ foreignLiveSpy.mockReturnValue(undefined);
125
+ ({ agentDir, service, store } = setup());
126
+ });
127
+
128
+ afterEach(() => {
129
+ vi.restoreAllMocks();
130
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
131
+ });
132
+
133
+ const stubColdCandidate = (record: {
134
+ id: string;
135
+ status: "running";
136
+ sessionFile: string;
137
+ rootSessionId: string;
138
+ parentRecordId?: string;
139
+ }): void => {
140
+ vi.spyOn(store, "findLightById").mockReturnValue(undefined);
141
+ vi.spyOn(store, "collectRecords").mockReturnValue([record] as never);
142
+ };
143
+
144
+ it("denies resurrection with ResurrectDeniedError when a foreign live instance holds the session", () => {
145
+ stubColdCandidate({
146
+ id: "sa-foreign",
147
+ status: "running",
148
+ sessionFile: "/tmp/fake-session.jsonl",
149
+ rootSessionId: "root-session",
150
+ });
151
+ foreignLiveSpy.mockReturnValue({ pid: 4242, id: "sa-foreign", startedAt: Date.now() });
152
+
153
+ expect(() => service["getRecordForAction"]("sa-foreign")).toThrow(ResurrectDeniedError);
154
+ try {
155
+ service["getRecordForAction"]("sa-foreign");
156
+ } catch (err) {
157
+ expect((err as Error).message).toContain("4242");
158
+ expect((err as Error).message).toContain("double-write");
159
+ expect((err as Error).message).toContain("Recovery");
160
+ }
161
+ expect(foreignLiveSpy).toHaveBeenCalledWith("/tmp/fake-session.jsonl");
162
+ });
163
+
164
+ it("allows cold lookup when no foreign live instance (marker dead/stale)", () => {
165
+ stubColdCandidate({
166
+ id: "sa-cold",
167
+ status: "running",
168
+ sessionFile: "/tmp/fake-session-2.jsonl",
169
+ rootSessionId: "root-session",
170
+ });
171
+ const found = service["getRecordForAction"]("sa-cold");
172
+ expect(found.id).toBe("sa-cold");
173
+ });
174
+ });