@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
@@ -1,10 +1,12 @@
1
- // pool-manager.test.ts —— 池 acquire/release 引用计数 / 归零整池删除 / journal 保留
2
- // / 清理失败标记 / spawnedFiles 单次清理(D5 + §3.3.9)。
1
+ // pool-manager.test.ts —— 池 acquire/release 引用计数(refs.json 文件形态)/
2
+ // 归零整池删除 / journal 保留与跟随 record 删除 / 重启恢复 / TTL 兜底 /
3
+ // 清理失败标记 / spawnedFiles 单次清理(D5 + §3.3.9 + D8 接线)。
3
4
  //
4
5
  // 三视角:①构建者——计数归零才删、删除边界三硬规则逐条;②使用者——release 幂等
5
6
  // 安全(无引用 no-op);③观察者——清理失败有 .pool-cleanup-failed 标记(可观测)。
6
7
 
7
- import { mkdirSync, mkdtempSync, existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import * as nodeFs from "node:fs";
9
+ import { mkdirSync, mkdtempSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync, utimesSync } from "node:fs";
8
10
  import { tmpdir } from "node:os";
9
11
  import { join } from "node:path";
10
12
 
@@ -12,10 +14,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
12
14
 
13
15
  import {
14
16
  acquirePool,
17
+ cleanupExpiredPoolRefs,
15
18
  cleanupSpawnedFiles,
16
19
  POOL_CLEANUP_FAILED_MARKER,
20
+ REFS_JSON_FILENAME,
17
21
  releasePoolRef,
18
- resetPoolRegistryForTests,
19
22
  } from "../../common/pool-manager.ts";
20
23
  import { resolvePoolDir } from "../../paths.ts";
21
24
 
@@ -23,132 +26,289 @@ let tmpRoot: string;
23
26
 
24
27
  beforeEach(() => {
25
28
  tmpRoot = mkdtempSync(join(tmpdir(), "engine-pool-test-"));
26
- resetPoolRegistryForTests();
27
29
  });
28
30
 
29
31
  afterEach(() => {
30
32
  rmSync(tmpRoot, { recursive: true, force: true });
31
33
  });
32
34
 
33
- describe("acquirePool", () => {
34
- it("mkdir -p 返回池目录(路径经 resolvePoolDir 派生),重复 acquire 幂等", async () => {
35
- const dir1 = await acquirePool(tmpRoot, "zcode", "reviewer");
36
- const dir2 = await acquirePool(tmpRoot, "zcode", "reviewer");
35
+ /** 读池 refs.json 的解析形态。 */
36
+ function readRefs(poolDir: string): { v: number; refs: Record<string, { taskId: string; ts: number }> } {
37
+ return JSON.parse(readFileSync(join(poolDir, REFS_JSON_FILENAME), "utf8")) as {
38
+ v: number;
39
+ refs: Record<string, { taskId: string; ts: number }>;
40
+ };
41
+ }
42
+
43
+ describe("acquirePool(refs.json 文件形态)", () => {
44
+ it("mkdir -p 返回池目录(路径经 resolvePoolDir 派生),refs.json 登记 taskId", () => {
45
+ const dir1 = acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
46
+ const dir2 = acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
37
47
  expect(dir1).toBe(resolvePoolDir(tmpRoot, "zcode", "reviewer"));
38
48
  expect(dir1).toBe(dir2);
39
49
  expect(existsSync(dir1)).toBe(true);
50
+ const refs = readRefs(dir1);
51
+ expect(refs.v).toBe(1);
52
+ expect(Object.keys(refs.refs)).toEqual(["bg-1"]);
53
+ expect(refs.refs["bg-1"]!.taskId).toBe("bg-1");
54
+ expect(typeof refs.refs["bg-1"]!.ts).toBe("number");
55
+ });
56
+
57
+ it("同 taskId 重复 acquire 幂等(刷新 ts 不加条目);不同 taskId 独立登记", () => {
58
+ const dir = acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
59
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
60
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-2");
61
+ expect(Object.keys(readRefs(dir).refs).sort()).toEqual(["bg-1", "bg-2"]);
40
62
  });
41
63
 
42
- it("poolKey 经 sanitizeSeg 归一(脏字符不进文件系统路径)", async () => {
43
- const dir = await acquirePool(tmpRoot, "zcode", "weird/key..name");
64
+ it("poolKey 经 sanitizeSeg 归一(脏字符不进文件系统路径)", () => {
65
+ const dir = acquirePool(tmpRoot, "zcode", "weird/key..name", "bg-1");
44
66
  expect(dir).toBe(resolvePoolDir(tmpRoot, "zcode", "weird-key-name"));
45
67
  });
68
+
69
+ it("refs.json 损坏 → 空登记起步(重建语义,不 throw)", () => {
70
+ const dir = acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
71
+ writeFileSync(join(dir, REFS_JSON_FILENAME), "{ torn", "utf8");
72
+ // 损坏后再次 acquire:从空 refs 起步,新 taskId 正常登记
73
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-9");
74
+ expect(Object.keys(readRefs(dir).refs)).toEqual(["bg-9"]);
75
+ });
46
76
  });
47
77
 
48
78
  describe("releasePoolRef 引用计数与整池删除", () => {
49
- it("双引用:release 一次池仍在(原生状态未删),归零才整池删除", async () => {
79
+ it("双引用(不同 taskId):release 一次池仍在(原生状态未删),归零才整池删除", () => {
50
80
  const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
51
- await acquirePool(tmpRoot, "zcode", "reviewer");
52
- await acquirePool(tmpRoot, "zcode", "reviewer");
81
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
82
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-2");
53
83
  mkdirSync(join(poolDir, "home"), { recursive: true });
54
84
  writeFileSync(join(poolDir, "home", "config.json"), "{}");
55
85
 
56
- await releasePoolRef(tmpRoot, "zcode", "reviewer");
86
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1");
57
87
  expect(existsSync(join(poolDir, "home"))).toBe(true); // 计数 1,不删
88
+ expect(Object.keys(readRefs(poolDir).refs)).toEqual(["bg-2"]);
58
89
 
59
- await releasePoolRef(tmpRoot, "zcode", "reviewer"); // 归零
90
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-2"); // 归零
60
91
  expect(existsSync(join(poolDir, "home"))).toBe(false); // 原生状态已删
61
92
  });
62
93
 
63
- it("journal-*.jsonl 不随池删(生命周期跟随 record),目录保留", async () => {
94
+ it("release 删该 taskId 的 journal(生命周期跟随 record),其他 journal 保留", () => {
64
95
  const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
65
- await acquirePool(tmpRoot, "zcode", "reviewer");
96
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
97
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-2");
66
98
  writeFileSync(join(poolDir, "journal-bg-1.jsonl"), "{}\n");
99
+ writeFileSync(join(poolDir, "journal-bg-2.jsonl"), "{}\n");
67
100
  mkdirSync(join(poolDir, "home"), { recursive: true });
68
101
 
69
- await releasePoolRef(tmpRoot, "zcode", "reviewer");
70
- expect(existsSync(join(poolDir, "journal-bg-1.jsonl"))).toBe(true);
71
- expect(existsSync(join(poolDir, "home"))).toBe(false);
102
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1");
103
+ expect(existsSync(join(poolDir, "journal-bg-1.jsonl"))).toBe(false); // 自己的 journal 随 record 删
104
+ expect(existsSync(join(poolDir, "journal-bg-2.jsonl"))).toBe(true); // 他人的 journal 不动
105
+ expect(existsSync(join(poolDir, "home"))).toBe(true); // 计数 1,原生状态不删
106
+ });
107
+
108
+ it("归零删池时未 release 的 journal 不随池删(目录保留)", () => {
109
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
110
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
111
+ // 孤儿 journal(无 refs 条目对应——refs 丢失场景的既有残留)
112
+ writeFileSync(join(poolDir, "journal-bg-99.jsonl"), "{}\n");
113
+ mkdirSync(join(poolDir, "home"), { recursive: true });
114
+
115
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1"); // 归零
116
+ expect(existsSync(join(poolDir, "home"))).toBe(false); // 原生状态已删
117
+ expect(existsSync(join(poolDir, "journal-bg-99.jsonl"))).toBe(true); // journal 不随池删
72
118
  // 只剩 journal:目录本体保留(journal 还在用)
73
119
  expect(existsSync(poolDir)).toBe(true);
74
- expect(readdirSync(poolDir)).toEqual(["journal-bg-1.jsonl"]);
120
+ expect(readdirSync(poolDir)).toEqual(["journal-bg-99.jsonl"]);
121
+ });
122
+
123
+ it("无引用的 release(进程重启后 refs 丢失)→ journal 仍删(record 已死),但保守不删池", () => {
124
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
125
+ mkdirSync(join(poolDir, "home"), { recursive: true });
126
+ writeFileSync(join(poolDir, "journal-bg-1.jsonl"), "{}\n");
127
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1");
128
+ expect(existsSync(join(poolDir, "home"))).toBe(true); // 无归零证据,不删池
129
+ expect(existsSync(join(poolDir, "journal-bg-1.jsonl"))).toBe(false); // journal 跟随 record
75
130
  });
76
131
 
77
- it("无引用的 release(进程重启后 GC)→ 保守 no-op 不删池", async () => {
132
+ it("refs.json 落盘即重启恢复:acquire 后不经内存态,release 一次即归零删池", () => {
78
133
  const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
134
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
79
135
  mkdirSync(join(poolDir, "home"), { recursive: true });
80
- await releasePoolRef(tmpRoot, "zcode", "reviewer");
81
- expect(existsSync(join(poolDir, "home"))).toBe(true);
136
+ writeFileSync(join(poolDir, "home", "config.json"), "{}");
137
+ // 「重启」语义 = 计数只存在于 refs.json(无进程内缓存)——直接 release 即验证
138
+ releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1");
139
+ expect(existsSync(join(poolDir, "home"))).toBe(false); // 归零证据来自文件
82
140
  });
83
141
 
84
- it("池目录不存在时 release 归零 → 无原生状态可清,视为成功不抛", async () => {
85
- await acquirePool(tmpRoot, "zcode", "ghost");
142
+ it("池目录不存在时 release → 无原生状态可清,视为成功不抛", () => {
143
+ acquirePool(tmpRoot, "zcode", "ghost", "bg-1");
86
144
  // 归零但目录已被外部删除
87
145
  rmSync(resolvePoolDir(tmpRoot, "zcode", "ghost"), { recursive: true, force: true });
88
- await expect(releasePoolRef(tmpRoot, "zcode", "ghost")).resolves.toBeUndefined();
146
+ expect(() => releasePoolRef(tmpRoot, "zcode", "ghost", "bg-1")).not.toThrow();
89
147
  });
90
148
 
91
- it("删除失败置 .pool-cleanup-failed 标记文件(可观测不静默),不 throw", async () => {
149
+ it("删除失败置 .pool-cleanup-failed 标记文件(可观测不静默),不 throw", () => {
92
150
  const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
93
- await acquirePool(tmpRoot, "zcode", "reviewer");
151
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
94
152
  mkdirSync(join(poolDir, "home"), { recursive: true });
95
- writeFileSync(join(poolDir, "journal-bg-1.jsonl"), "{}\n");
153
+ writeFileSync(join(poolDir, "journal-bg-2.jsonl"), "{}\n");
96
154
 
97
- // 注入失败 fs:rm 对 home 目录 reject(标记写入仍走真实 fs——失败要可观测)
98
- const realFs = await import("node:fs/promises");
155
+ // 注入失败 fs:rmSync 对 home 目录 throw(标记写入仍走真实 fs——失败要可观测)
156
+ const fs = nodeFs;
99
157
  const injected = {
100
- mkdir: (p: string, o: { recursive: boolean }) => realFs.mkdir(p, o),
101
- readdir: (p: string) => realFs.readdir(p, { withFileTypes: true }),
102
- rm: async (p: string, o: { recursive: boolean; force: boolean }) => {
158
+ mkdirSync: (p: string, o: { recursive: boolean }) => fs.mkdirSync(p, o),
159
+ readdirSync: (p: string) => fs.readdirSync(p, { withFileTypes: true }),
160
+ statSync: (p: string) => fs.statSync(p),
161
+ existsSync: (p: string) => fs.existsSync(p),
162
+ readFileSync: (p: string) => fs.readFileSync(p, "utf8"),
163
+ writeFileSync: (p: string, d: string) => fs.writeFileSync(p, d, "utf8"),
164
+ renameSync: (from: string, to: string) => fs.renameSync(from, to),
165
+ rmSync: (p: string, o: { recursive: boolean; force: boolean }) => {
103
166
  if (p === join(poolDir, "home")) throw new Error("EACCES: permission denied");
104
- return realFs.rm(p, o);
167
+ return fs.rmSync(p, o);
105
168
  },
106
- rmdir: (p: string) => realFs.rmdir(p),
107
- writeFile: (p: string, d: string) => realFs.writeFile(p, d, "utf8"),
169
+ rmdirSync: (p: string) => fs.rmdirSync(p),
108
170
  };
109
171
 
110
- await expect(releasePoolRef(tmpRoot, "zcode", "reviewer", injected)).resolves.toBeUndefined();
172
+ expect(() => releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1", injected)).not.toThrow();
111
173
  const markerPath = join(poolDir, POOL_CLEANUP_FAILED_MARKER);
112
174
  expect(existsSync(markerPath)).toBe(true);
113
175
  // 标记内容是失败清单 JSON(含失败条目与原因)
114
- const payload = JSON.parse(
115
- (await import("node:fs")).readFileSync(markerPath, "utf8"),
116
- ) as { failures: string[] };
176
+ const payload = JSON.parse(readFileSync(markerPath, "utf8")) as { failures: string[] };
117
177
  expect(payload.failures.length).toBe(1);
118
178
  expect(payload.failures[0]).toContain("home");
119
179
  expect(payload.failures[0]).toContain("EACCES");
120
180
  });
181
+
182
+ it("refs.json 写失败(release 移除条目时)置 .pool-cleanup-failed 标记,不 throw", () => {
183
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
184
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-1");
185
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-2");
186
+
187
+ const fs = nodeFs;
188
+ const injected = {
189
+ mkdirSync: (p: string, o: { recursive: boolean }) => fs.mkdirSync(p, o),
190
+ readdirSync: (p: string) => fs.readdirSync(p, { withFileTypes: true }),
191
+ statSync: (p: string) => fs.statSync(p),
192
+ existsSync: (p: string) => fs.existsSync(p),
193
+ readFileSync: (p: string) => fs.readFileSync(p, "utf8"),
194
+ writeFileSync: (p: string, d: string) => {
195
+ if (p.includes(REFS_JSON_FILENAME)) throw new Error("ENOSPC: no space left");
196
+ return fs.writeFileSync(p, d, "utf8");
197
+ },
198
+ renameSync: (from: string, to: string) => fs.renameSync(from, to),
199
+ rmSync: (p: string, o: { recursive: boolean; force: boolean }) => fs.rmSync(p, o),
200
+ rmdirSync: (p: string) => fs.rmdirSync(p),
201
+ };
202
+
203
+ expect(() => releasePoolRef(tmpRoot, "zcode", "reviewer", "bg-1", injected)).not.toThrow();
204
+ expect(existsSync(join(poolDir, POOL_CLEANUP_FAILED_MARKER))).toBe(true);
205
+ // 池未被误删(写失败时无归零证据落盘)
206
+ expect(existsSync(poolDir)).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe("cleanupExpiredPoolRefs(TTL 兜底)", () => {
211
+ /** 30 天 TTL 常数(与 session-file-gc 同尺度)。 */
212
+ const TTL_MS = 30 * 24 * 60 * 60 * 1000;
213
+
214
+ it("超龄 journal + refs 条目回收,归零删池内原生状态;未超龄不动", () => {
215
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
216
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-old");
217
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-new");
218
+ mkdirSync(join(poolDir, "home"), { recursive: true });
219
+ writeFileSync(join(poolDir, "journal-bg-old.jsonl"), "{}\n");
220
+ writeFileSync(join(poolDir, "journal-bg-new.jsonl"), "{}\n");
221
+
222
+ // mtime 推到 31 天前(仅 old 的 journal)
223
+ const aged = Date.now() / 1000 - 31 * 86400;
224
+ utimesSync(join(poolDir, "journal-bg-old.jsonl"), aged, aged);
225
+
226
+ cleanupExpiredPoolRefs(tmpRoot, TTL_MS);
227
+
228
+ expect(existsSync(join(poolDir, "journal-bg-old.jsonl"))).toBe(false); // 超龄回收
229
+ expect(existsSync(join(poolDir, "journal-bg-new.jsonl"))).toBe(true); // 未超龄保留
230
+ expect(Object.keys(readRefs(poolDir).refs)).toEqual(["bg-new"]); // 条目同步移除
231
+ expect(existsSync(join(poolDir, "home"))).toBe(true); // 未归零,原生状态不删
232
+ });
233
+
234
+ it("全部条目超龄 → 归零删池原生状态 + refs.json;未超龄 journal 挡住目录删除", () => {
235
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
236
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-old");
237
+ mkdirSync(join(poolDir, "home"), { recursive: true });
238
+ writeFileSync(join(poolDir, "journal-bg-old.jsonl"), "{}\n");
239
+ // 无 refs 对应的未超龄孤儿 journal(refs 丢失场景)——目录应保留
240
+ writeFileSync(join(poolDir, "journal-bg-live.jsonl"), "{}\n");
241
+
242
+ const aged = Date.now() / 1000 - 31 * 86400;
243
+ utimesSync(join(poolDir, "journal-bg-old.jsonl"), aged, aged);
244
+
245
+ cleanupExpiredPoolRefs(tmpRoot, TTL_MS);
246
+
247
+ expect(existsSync(join(poolDir, "journal-bg-old.jsonl"))).toBe(false);
248
+ expect(existsSync(join(poolDir, "journal-bg-live.jsonl"))).toBe(true);
249
+ expect(existsSync(join(poolDir, "home"))).toBe(false); // 原生状态删
250
+ expect(existsSync(join(poolDir, REFS_JSON_FILENAME))).toBe(false); // 空池 refs.json 随归零清理
251
+ expect(existsSync(poolDir)).toBe(true); // 剩未超龄 journal,目录保留
252
+ });
253
+
254
+ it("refs 条目 ts 超龄且 journal 不存在 → 孤儿条目移除(防 refs 永久留置)", () => {
255
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
256
+ acquirePool(tmpRoot, "zcode", "reviewer", "bg-orphan");
257
+ // journal 从未落盘(run 从未产出事件);条目 ts 已超龄 → 移除 → 归零(池内仅
258
+ // refs.json,随归零清理,目录随之移除)
259
+ const refsPath = join(poolDir, REFS_JSON_FILENAME);
260
+ const refs = JSON.parse(readFileSync(refsPath, "utf8")) as { refs: Record<string, { ts: number }> };
261
+ refs.refs["bg-orphan"]!.ts = Date.now() - 31 * 86400 * 1000;
262
+ writeFileSync(refsPath, JSON.stringify(refs));
263
+
264
+ cleanupExpiredPoolRefs(tmpRoot, TTL_MS);
265
+ // 归零路径:refs.json 清理(无幻影引用残留);空目录本体保留——与「只剩 journal
266
+ // 保留目录」同语义(防与并发 acquire 的 mkdir/写 refs 竞争,目录无害)
267
+ expect(existsSync(refsPath)).toBe(false);
268
+ expect(readdirSync(poolDir)).toEqual([]);
269
+ });
270
+
271
+ it("无主超龄 journal(refs 丢失)回收;engines 根不存在时 no-op", () => {
272
+ const poolDir = resolvePoolDir(tmpRoot, "zcode", "reviewer");
273
+ mkdirSync(poolDir, { recursive: true });
274
+ writeFileSync(join(poolDir, "journal-bg-x.jsonl"), "{}\n");
275
+ const aged = Date.now() / 1000 - 31 * 86400;
276
+ utimesSync(join(poolDir, "journal-bg-x.jsonl"), aged, aged);
277
+
278
+ cleanupExpiredPoolRefs(tmpRoot, TTL_MS);
279
+ expect(existsSync(join(poolDir, "journal-bg-x.jsonl"))).toBe(false);
280
+
281
+ expect(() => cleanupExpiredPoolRefs(join(tmpRoot, "absent"), TTL_MS)).not.toThrow();
282
+ });
121
283
  });
122
284
 
123
285
  describe("cleanupSpawnedFiles(单次性产物)", () => {
124
- it("keepForResume=true → 全部保留(resume 续接原 session,不重写产物)", async () => {
286
+ it("keepForResume=true → 全部保留(resume 续接原 session,不重写产物)", () => {
125
287
  const f = join(tmpRoot, "prompt.txt");
126
288
  writeFileSync(f, "x");
127
- await cleanupSpawnedFiles([f], { keepForResume: true });
289
+ cleanupSpawnedFiles([f], { keepForResume: true });
128
290
  expect(existsSync(f)).toBe(true);
129
291
  });
130
292
 
131
- it("keepForResume=false → 删除;目录产物递归删", async () => {
293
+ it("keepForResume=false → 删除;目录产物递归删", () => {
132
294
  const file = join(tmpRoot, "prompt.txt");
133
295
  const dir = join(tmpRoot, "persona");
134
296
  writeFileSync(file, "x");
135
297
  mkdirSync(dir, { recursive: true });
136
298
  writeFileSync(join(dir, "persona.md"), "y");
137
- await cleanupSpawnedFiles([file, dir], { keepForResume: false });
299
+ cleanupSpawnedFiles([file, dir], { keepForResume: false });
138
300
  expect(existsSync(file)).toBe(false);
139
301
  expect(existsSync(dir)).toBe(false);
140
302
  });
141
303
 
142
- it("不存在的路径幂等(ENOENT 忽略);单条失败不 throw 其余继续", async () => {
304
+ it("不存在的路径幂等(ENOENT 忽略);单条失败不 throw 其余继续", () => {
143
305
  const good = join(tmpRoot, "good.txt");
144
306
  writeFileSync(good, "x");
145
307
  // 真实 fs 下很难制造 rm 失败——用「父路径为文件」制造 ENOTDIR 失败:
146
308
  // rm(recursive, force) 对 «file/child» 形态报 ENOTDIR(force 只豁免 ENOENT)
147
309
  const blocked = join(tmpRoot, "plain-file", "child");
148
310
  writeFileSync(join(tmpRoot, "plain-file"), "not a dir");
149
- await expect(
150
- cleanupSpawnedFiles([blocked, good], { keepForResume: false }),
151
- ).resolves.toBeUndefined();
311
+ expect(() => cleanupSpawnedFiles([blocked, good], { keepForResume: false })).not.toThrow();
152
312
  expect(existsSync(good)).toBe(false);
153
313
  });
154
314
  });
@@ -0,0 +1,149 @@
1
+ // session-view-service-zcode-dbpath.test.ts —— zcode ①级 dbPath 白名单的分支守护。
2
+ //
3
+ // 三视角:①构建者——白名单三分支(绝对=宿主 db 精确匹配 / 绝对≠宿主 db 拒绝 /
4
+ // 相对池锚定 + 越界拒绝)逐路径断言;②使用者——共享 HOME 形态 record(写侧
5
+ // zcode-engine 恒绝对 dbPath)tier1 不再被误拒静默降②级;③观察者——reader 调用
6
+ // 参数(白名单通过后传入的 dbPath 精确值)与未调用事实(拒绝路径 reader 零触达)。
7
+ //
8
+ // readZcodeSessionView 以 vi.mock 替身:本文件测的是白名单分支(不可触达真实
9
+ // ~/.zcode/cli/db/db.sqlite——那是用户真机数据),reader 本体行为由
10
+ // engines/zcode/__tests__/reader.test.ts 真库覆盖。
11
+
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { homedir, tmpdir } from "node:os";
14
+ import { join, resolve } from "node:path";
15
+
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
+
18
+ vi.mock("../../engines/zcode/reader.ts", () => ({
19
+ readZcodeSessionView: vi.fn(),
20
+ }));
21
+
22
+ import { readZcodeSessionView } from "../../engines/zcode/reader.ts";
23
+ import { ZCODE_HOST_DB_SUFFIX } from "../../engines/zcode/constants.ts";
24
+ import { JournalWriter } from "../../common/event-journal.ts";
25
+ import { readSubagentHistoryMessages } from "../../common/session-view-service.ts";
26
+ import { resetNativeSessionReaders } from "../../common/session-view-service.ts";
27
+ import type { SubagentRecordSnapshot } from "../../common/session-view-types.ts";
28
+ import type { AgentEvent } from "../../../types.ts";
29
+ import type { SessionView } from "../../types.ts";
30
+
31
+ const mockedRead = vi.mocked(readZcodeSessionView);
32
+
33
+ /** 共享 HOME 形态写侧恒写入的唯一合法绝对 dbPath(与 zcode-engine hostZcodeDbPath 同源推导)。 */
34
+ const HOST_DB = resolve(homedir(), ...ZCODE_HOST_DB_SUFFIX);
35
+
36
+ let dataDir: string;
37
+
38
+ beforeEach(() => {
39
+ dataDir = mkdtempSync(join(tmpdir(), "svs-dbpath-test-"));
40
+ mockedRead.mockReset();
41
+ });
42
+
43
+ afterEach(() => {
44
+ rmSync(dataDir, { recursive: true, force: true });
45
+ resetNativeSessionReaders();
46
+ });
47
+
48
+ function makeRecord(overrides: Partial<SubagentRecordSnapshot> = {}): SubagentRecordSnapshot {
49
+ return {
50
+ subagentId: "sub-1",
51
+ task: "do the thing",
52
+ startedAt: 1_000,
53
+ engine: "zcode",
54
+ engineHandle: {
55
+ sessionRef: { sessionId: "sess-1", dbPath: HOST_DB },
56
+ poolKey: "shared",
57
+ },
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ async function writeJournal(path: string, events: AgentEvent[]): Promise<void> {
63
+ const writer = new JournalWriter({ path, taskId: "sub-1", engineId: "zcode" });
64
+ for (const ev of events) writer.append(ev);
65
+ await writer.close();
66
+ }
67
+
68
+ /** record 带②级可达 journal(tier1 被拒后的可观察落点:journal 内容被投影)。 */
69
+ async function journalRecord(dbPath: string): Promise<SubagentRecordSnapshot> {
70
+ const journalPath = join(dataDir, "engines", "zcode", "shared", "journal-sub-1.jsonl");
71
+ await writeJournal(journalPath, [{ type: "text_delta", delta: "journal says" }]);
72
+ return makeRecord({
73
+ result: "outcome fallback",
74
+ engineHandle: {
75
+ sessionRef: { sessionId: "sess-1", dbPath },
76
+ poolKey: "shared",
77
+ journalPath,
78
+ },
79
+ });
80
+ }
81
+
82
+ describe("zcode ①级 dbPath 白名单(分支守护)", () => {
83
+ it("绝对 dbPath = 宿主 db(SSOT 精确匹配)→ 白名单通过,reader 收到原样路径,tier1 命中", async () => {
84
+ const view: SessionView = {
85
+ engineId: "zcode",
86
+ sessionId: "sess-1",
87
+ turns: [{ text: "from host db", thinking: "", toolCalls: [], closed: true }],
88
+ source: "native",
89
+ };
90
+ mockedRead.mockResolvedValue(view);
91
+ const messages = await readSubagentHistoryMessages(makeRecord(), dataDir);
92
+ expect(mockedRead).toHaveBeenCalledTimes(1);
93
+ expect(mockedRead).toHaveBeenCalledWith(HOST_DB, "sess-1");
94
+ expect(messages[1]).toMatchObject({ role: "assistant", content: "from host db" });
95
+ });
96
+
97
+ it("绝对 dbPath ≠ 宿主 db → 拒绝 tier1(reader 零触达),降②级 journal", async () => {
98
+ const record = await journalRecord("/tmp/attacker-chosen/db.sqlite");
99
+ const messages = await readSubagentHistoryMessages(record, dataDir);
100
+ expect(mockedRead).not.toHaveBeenCalled();
101
+ expect(messages[1]).toMatchObject({ role: "assistant", content: "journal says" });
102
+ });
103
+
104
+ it("宿主 db 路径的目录内变体(/../ 注入)≠ 精确匹配 → 同样拒绝", async () => {
105
+ // 白名单是文本级精确匹配(resolve 不可用于反构造——会把 .. 规约掉变回正身)
106
+ const sneaky = `${HOST_DB}/../db.sqlite`;
107
+ const record = await journalRecord(sneaky);
108
+ const messages = await readSubagentHistoryMessages(record, dataDir);
109
+ expect(mockedRead).not.toHaveBeenCalled();
110
+ expect(messages[1]).toMatchObject({ content: "journal says" });
111
+ });
112
+
113
+ it("相对 dbPath 池内(旧池时代 record)→ 锚池目录解析,reader 收到池内绝对路径", async () => {
114
+ const view: SessionView = {
115
+ engineId: "zcode",
116
+ sessionId: "sess-1",
117
+ turns: [{ text: "legacy pool", thinking: "", toolCalls: [], closed: true }],
118
+ source: "native",
119
+ };
120
+ mockedRead.mockResolvedValue(view);
121
+ const record = makeRecord({
122
+ engineHandle: {
123
+ sessionRef: { sessionId: "sess-1", dbPath: "pool-db/db.sqlite" },
124
+ poolKey: "shared",
125
+ },
126
+ });
127
+ const messages = await readSubagentHistoryMessages(record, dataDir);
128
+ expect(mockedRead).toHaveBeenCalledWith(
129
+ join(dataDir, "engines", "zcode", "shared", "pool-db", "db.sqlite"),
130
+ "sess-1",
131
+ );
132
+ expect(messages[1]).toMatchObject({ content: "legacy pool" });
133
+ });
134
+
135
+ it("相对 dbPath ../ 逃逸池目录 → 拒绝 tier1(reader 零触达),降②级 journal", async () => {
136
+ const record = await journalRecord("../../escape/db.sqlite");
137
+ const messages = await readSubagentHistoryMessages(record, dataDir);
138
+ expect(mockedRead).not.toHaveBeenCalled();
139
+ expect(messages[1]).toMatchObject({ content: "journal says" });
140
+ });
141
+
142
+ it("白名单通过但 reader 抛错(结构化降级契约)→ 仍降②级不崩", async () => {
143
+ mockedRead.mockRejectedValue(new Error("engine_session_read_failed: db 文件不存在"));
144
+ const record = await journalRecord(HOST_DB);
145
+ const messages = await readSubagentHistoryMessages(record, dataDir);
146
+ expect(mockedRead).toHaveBeenCalledTimes(1);
147
+ expect(messages[1]).toMatchObject({ content: "journal says" });
148
+ });
149
+ });