@zhushanwen/subagent-core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,199 @@
1
+ // src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts
2
+ //
3
+ // [F-R2] stdout data 同步回调链内 fail-fast throw 不逃逸(不升级为 uncaughtException 崩宿主)。
4
+ //
5
+ // 背景:session-runner 的两处 timer 安全校验调用位于 child.stdout.on("data") 同步回调链内:
6
+ // ① agent_end keep-alive 分支的 resolveSpawnWatchdogMs(→ assertSafeTimerDelay fail-fast)
7
+ // ② chatMode agent_settled 分支的 armIdleTimer(→ assertSafeTimerDelay fail-fast)
8
+ // 旧实现任一处 throw 都会逃出事件回调 = Node uncaughtException 崩宿主进程。
9
+ // 修复:调用点包 try/catch 降级(不挂 timer),错误经 bestEffort("error") 可见。
10
+ //
11
+ // mock 结构与 run-spawn-chatmode-settled.test.ts 一致(FakeChild + lifecycle-manager 真实模块)。
12
+ // logger 整体 mock(loggerMock spy)断言错误可见——「fail-fast 语义保留(错误可见、行为明确)
13
+ // 但不升级为进程崩溃」。
14
+ import { spawn } from "node:child_process";
15
+ import * as fs from "node:fs";
16
+
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+
19
+ // logger mock:bestEffort / session-runner 共享同一 loggerMock(断言错误可见性)。
20
+ // vi.hoisted:vi.mock 工厂被提升到顶层 const 之前执行,loggerMock 必须经 vi.hoisted 创建。
21
+ const { loggerMock } = vi.hoisted(() => ({
22
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
23
+ }));
24
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
25
+
26
+ vi.mock("node:child_process", async () => {
27
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
28
+ return {
29
+ spawn: vi.fn(() => new FakeChild()),
30
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
31
+ execFile: vi.fn(
32
+ (
33
+ _cmd: string,
34
+ _args: readonly string[],
35
+ _opts: unknown,
36
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
37
+ ) => cb(new Error("execFile not configured in this test")),
38
+ ),
39
+ };
40
+ });
41
+
42
+ vi.mock("node:fs", async () => {
43
+ const actual = await import("node:fs");
44
+ return {
45
+ default: {
46
+ ...actual,
47
+ mkdirSync: vi.fn(),
48
+ existsSync: vi.fn(() => false),
49
+ appendFileSync: vi.fn(),
50
+ writeFileSync: vi.fn(),
51
+ readdirSync: vi.fn(() => []),
52
+ },
53
+ mkdirSync: vi.fn(),
54
+ existsSync: vi.fn(() => false),
55
+ appendFileSync: vi.fn(),
56
+ writeFileSync: vi.fn(),
57
+ readdirSync: vi.fn(() => []),
58
+ promises: actual.promises,
59
+ };
60
+ });
61
+
62
+ vi.mock("../alive-store.ts", () => ({
63
+ writeAliveMarker: vi.fn(),
64
+ }));
65
+
66
+ // 场景 ① 依赖 keep-alive 分支命中:count=1(有活跃后代 → 不 kill → 重挂 watchdog)
67
+ vi.mock("../session-pending.ts", () => ({
68
+ readActivePendingFromSessionFile: vi.fn(() => ({ count: 1 })),
69
+ }));
70
+
71
+ vi.mock("../temp-prompt.ts", () => ({
72
+ writePromptToTempFile: vi.fn(async (agent: string) => {
73
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
74
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
75
+ }),
76
+ cleanupTempPrompt: vi.fn(async () => {}),
77
+ }));
78
+
79
+ import { runSpawn, SPAWN_WATCHDOG_ENV } from "../session-runner.ts";
80
+ import type { SessionRunnerContext } from "../session-runner.ts";
81
+ import { hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
82
+ import { createRecord } from "../execution-record.ts";
83
+ import type { ExecutionRecord } from "../types.ts";
84
+ import {
85
+ emitStdoutLine,
86
+ type FakeChild,
87
+ lastSpawnedChild as lastSpawnedChildOf,
88
+ makeCtx,
89
+ makeOpts,
90
+ makeRecord,
91
+ sessionHeader,
92
+ waitForSpawn as waitForSpawnOf,
93
+ } from "./helpers/spawn-mock.ts";
94
+
95
+ const mockSpawn = vi.mocked(spawn);
96
+
97
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
98
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
99
+
100
+ /** 构造 chatMode record(idleTimeoutMs 设为超出 setTimeout 安全域的值 → assertSafeTimerDelay throw)。 */
101
+ function makeChatModeRecord(id = "sa-f2-idle"): ExecutionRecord {
102
+ return createRecord(id, {
103
+ agent: "general-purpose",
104
+ model: "test-model",
105
+ mode: "sync",
106
+ task: "chat task",
107
+ slug: "chat",
108
+ startedAt: 1_000_000,
109
+ rootSessionId: "root-session",
110
+ parentRecordId: undefined,
111
+ depth: 0,
112
+ chatMode: true,
113
+ // > 2^31-1:Node setTimeout 溢出域,assertSafeTimerDelay fail-fast
114
+ idleTimeoutMs: Number.MAX_SAFE_INTEGER,
115
+ });
116
+ }
117
+
118
+ describe("[F-R2] stdout 回调链内 fail-fast throw 不逃逸", () => {
119
+ beforeEach(() => {
120
+ vi.clearAllMocks();
121
+ _resetLifecycleState();
122
+ });
123
+
124
+ afterEach(() => {
125
+ vi.restoreAllMocks();
126
+ _resetLifecycleState();
127
+ });
128
+
129
+ it("① agent_end keep-alive 分支 resolveSpawnWatchdogMs throw → 降级不 re-arm,run 正常收尾,error 可见", async () => {
130
+ const record = makeRecord();
131
+ const promise = runSpawn(record, "Task: keepalive", makeOpts(), makeCtx());
132
+
133
+ await waitForSpawn();
134
+ const child = lastSpawnedChild();
135
+
136
+ emitStdoutLine(child, sessionHeader("sess-fr2a"));
137
+ // 初始 watchdog 解析在 spawn 后同步块内已完成(env 未设 → 不挂)。
138
+ // 此刻设非法 env:agent_end keep-alive 分支再读 env → assertSafeTimerDelay throw。
139
+ process.env[SPAWN_WATCHDOG_ENV] = String(Number.MAX_SAFE_INTEGER);
140
+ try {
141
+ emitStdoutLine(child, { type: "agent_end", willRetry: false });
142
+ // 给 stream flush 留一个 tick(对齐 chatmode 测试模式):throw 若逃逸回调,
143
+ // vitest 会以 unhandled error 判本测试失败——能走到断言即「未崩宿主」。
144
+ await new Promise((r) => setTimeout(r, 20));
145
+
146
+ // 降级语义:不 re-arm(无 timer 可直接观察的是——子进程未被 kill、run 未被中断)
147
+ expect(child.killed).toBe(false);
148
+
149
+ // 错误可见(fail-fast 语义保留):bestEffort("error") 经 logger.error 落日志
150
+ expect(loggerMock.error).toHaveBeenCalledWith(
151
+ expect.stringContaining("resolveSpawnWatchdogMs"),
152
+ expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
153
+ );
154
+
155
+ // 收尾:run 正常完成(未被回调异常打断)
156
+ child.stdout.end();
157
+ child.stderr.end();
158
+ child.emit("close", 0);
159
+ const result = await promise;
160
+ expect(result.success).toBe(true);
161
+ } finally {
162
+ delete process.env[SPAWN_WATCHDOG_ENV];
163
+ }
164
+ });
165
+
166
+ it("② chatMode agent_settled armIdleTimer throw → 降级不挂 idle timer,onRoundSettled 照常、提前 resolve", async () => {
167
+ const record = makeChatModeRecord("sa-f2-idle");
168
+ const onRoundSettled = vi.fn();
169
+ const ctx: Partial<SessionRunnerContext> = { onRoundSettled };
170
+ const promise = runSpawn(record, "Task: idle", makeOpts(), makeCtx(ctx as SessionRunnerContext));
171
+
172
+ await waitForSpawn();
173
+ const child = lastSpawnedChild();
174
+
175
+ emitStdoutLine(child, sessionHeader("sess-fr2b"));
176
+ emitStdoutLine(child, { type: "agent_settled" });
177
+ await new Promise((r) => setTimeout(r, 20));
178
+
179
+ // 降级语义:idle timer 未挂(throw 发生在 arm 入口校验,先于 setTimeout)
180
+ expect(hasIdleTimer(record.id)).toBe(false);
181
+ expect(child.killed).toBe(false);
182
+
183
+ // catch 后续语句照常执行:本轮完成通知不因 GC timer 故障丢失
184
+ expect(onRoundSettled).toHaveBeenCalledTimes(1);
185
+
186
+ // 错误可见
187
+ expect(loggerMock.error).toHaveBeenCalledWith(
188
+ expect.stringContaining("armIdleTimer"),
189
+ expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
190
+ );
191
+
192
+ // 收尾:chatMode 首轮 agent_settled 已 resolveRun(0),close 后 runSpawn 正常返回
193
+ child.stdout.end();
194
+ child.stderr.end();
195
+ child.emit("close", 0);
196
+ const result = await promise;
197
+ expect(result.success).toBe(true);
198
+ });
199
+ });
@@ -0,0 +1,167 @@
1
+ // src/__tests__/session-context-resolver.test.ts
2
+ //
3
+ // 覆盖 resolveSessionContext 全部 5 种输入组合 + ForkDepthExceededError 边界。
4
+ import * as path from "node:path";
5
+
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import { encodeCwd } from "../path-encoding.ts";
9
+ import { resolveSessionContext } from "../session-context-resolver.ts";
10
+ import { ForkDepthExceededError } from "../types.ts";
11
+
12
+ const AGENT_DIR = "/home/user/.pi/agent";
13
+ const MAIN_CWD = "/home/user/project";
14
+ const SESSION_FILE = "session-abc.jsonl";
15
+
16
+ function expectedSessionDir(mainCwd: string): string {
17
+ // [MF#1] 既有布局:subagents/<enc>/sessions/
18
+ return path.join(AGENT_DIR, "subagents", encodeCwd(mainCwd), "sessions");
19
+ }
20
+
21
+ describe("resolveSessionContext", () => {
22
+ // ── 边界 1: fork=false, worktree=false ──
23
+ it("fork=false, worktree=false → shouldFork=false, effectiveCwd=mainCwd", () => {
24
+ const result = resolveSessionContext({
25
+ fork: false,
26
+ worktree: false,
27
+ mainCwd: MAIN_CWD,
28
+ agentDir: AGENT_DIR,
29
+ });
30
+ expect(result.shouldFork).toBe(false);
31
+ expect(result.forkSource).toBeUndefined();
32
+ expect(result.effectiveCwd).toBe(MAIN_CWD);
33
+ expect(result.sessionDir).toBe(expectedSessionDir(MAIN_CWD));
34
+ });
35
+
36
+ // ── 边界 2: fork=true, worktree=false ──
37
+ it("fork=true, worktree=false → shouldFork=true, forkSource=mainSessionFile", () => {
38
+ const result = resolveSessionContext({
39
+ fork: true,
40
+ worktree: false,
41
+ mainCwd: MAIN_CWD,
42
+ mainSessionFile: SESSION_FILE,
43
+ agentDir: AGENT_DIR,
44
+ });
45
+ expect(result.shouldFork).toBe(true);
46
+ expect(result.forkSource).toBe(SESSION_FILE);
47
+ expect(result.effectiveCwd).toBe(MAIN_CWD);
48
+ expect(result.sessionDir).toBe(expectedSessionDir(MAIN_CWD));
49
+ });
50
+
51
+ // ── 边界 3: fork=false, worktree=true ──
52
+ it("fork=false, worktreePath 提供 → effectiveCwd=worktreePath", () => {
53
+ const worktreePath = "/tmp/pi-sub-run-42";
54
+ const result = resolveSessionContext({
55
+ fork: false,
56
+ mainCwd: MAIN_CWD,
57
+ agentDir: AGENT_DIR,
58
+ worktreePath,
59
+ });
60
+ expect(result.shouldFork).toBe(false);
61
+ expect(result.forkSource).toBeUndefined();
62
+ expect(result.effectiveCwd).toBe(worktreePath);
63
+ expect(result.sessionDir).toBe(expectedSessionDir(MAIN_CWD));
64
+ });
65
+
66
+ // ── 边界 4: fork=true, worktree=true ──
67
+ it("fork=true, worktreePath 提供 → shouldFork=true + effectiveCwd=worktreePath", () => {
68
+ const worktreePath = "/tmp/pi-sub-bg-7-abc";
69
+ const result = resolveSessionContext({
70
+ fork: true,
71
+ mainCwd: MAIN_CWD,
72
+ mainSessionFile: SESSION_FILE,
73
+ agentDir: AGENT_DIR,
74
+ worktreePath,
75
+ });
76
+ expect(result.shouldFork).toBe(true);
77
+ expect(result.forkSource).toBe(SESSION_FILE);
78
+ expect(result.effectiveCwd).toBe(worktreePath);
79
+ expect(result.sessionDir).toBe(expectedSessionDir(MAIN_CWD));
80
+ });
81
+
82
+ // ── 边界 5: fork=true + parentForkDepth>=10 → 抛错 ──
83
+ it("fork=true + parentForkDepth=10 → throws ForkDepthExceededError", () => {
84
+ expect(() =>
85
+ resolveSessionContext({
86
+ fork: true,
87
+ mainCwd: MAIN_CWD,
88
+ mainSessionFile: SESSION_FILE,
89
+ parentForkDepth: 10,
90
+ agentDir: AGENT_DIR,
91
+ }),
92
+ ).toThrow(ForkDepthExceededError);
93
+ });
94
+
95
+ it("fork=true + parentForkDepth=15 → throws ForkDepthExceededError", () => {
96
+ expect(() =>
97
+ resolveSessionContext({
98
+ fork: true,
99
+ mainCwd: MAIN_CWD,
100
+ parentForkDepth: 15,
101
+ agentDir: AGENT_DIR,
102
+ }),
103
+ ).toThrow(ForkDepthExceededError);
104
+ });
105
+
106
+ // ── depth=9 不抛 ──
107
+ it("fork=true + parentForkDepth=9 → does NOT throw", () => {
108
+ const result = resolveSessionContext({
109
+ fork: true,
110
+ mainCwd: MAIN_CWD,
111
+ mainSessionFile: SESSION_FILE,
112
+ parentForkDepth: 9,
113
+ agentDir: AGENT_DIR,
114
+ });
115
+ expect(result.shouldFork).toBe(true);
116
+ });
117
+
118
+ // ── sessionDir 始终用 mainCwd 编码(非 effectiveCwd)──
119
+ it("sessionDir always encoded from mainCwd even when worktreePath overrides effectiveCwd", () => {
120
+ const mainCwd = "/Users/alice/code/my-app";
121
+ const result = resolveSessionContext({
122
+ fork: false,
123
+ mainCwd,
124
+ agentDir: AGENT_DIR,
125
+ worktreePath: "/tmp/pi-sub-run-1",
126
+ });
127
+ // effectiveCwd 是 worktree checkout,但 sessionDir 仍基于 mainCwd
128
+ expect(result.effectiveCwd).not.toBe(mainCwd);
129
+ expect(result.sessionDir).toBe(expectedSessionDir(mainCwd));
130
+ });
131
+
132
+ // ── [MF#5] fork=true 但主 session 文件不可用 → 抛错(不静默降级)──
133
+ it("fork=true + mainSessionFile 缺失 → throws(不静默降级到 from-scratch)", () => {
134
+ expect(() =>
135
+ resolveSessionContext({
136
+ fork: true,
137
+ mainCwd: MAIN_CWD,
138
+ // mainSessionFile 故意不传
139
+ agentDir: AGENT_DIR,
140
+ }),
141
+ ).toThrow(/main session file is unavailable/);
142
+ });
143
+
144
+ // ── 默认值:fork/worktree 未传 → undefined → false ──
145
+ it("fork/worktree omitted → shouldFork=false", () => {
146
+ const result = resolveSessionContext({
147
+ mainCwd: MAIN_CWD,
148
+ agentDir: AGENT_DIR,
149
+ });
150
+ expect(result.shouldFork).toBe(false);
151
+ expect(result.forkSource).toBeUndefined();
152
+ expect(result.effectiveCwd).toBe(MAIN_CWD);
153
+ });
154
+
155
+ // ── cwd 覆盖 mainCwd(worktree=false 时)──
156
+ it("explicit cwd overrides mainCwd when worktree=false", () => {
157
+ const customCwd = "/tmp/custom-cwd";
158
+ const result = resolveSessionContext({
159
+ mainCwd: MAIN_CWD,
160
+ cwd: customCwd,
161
+ agentDir: AGENT_DIR,
162
+ });
163
+ expect(result.effectiveCwd).toBe(customCwd);
164
+ // sessionDir 仍用 mainCwd
165
+ expect(result.sessionDir).toBe(expectedSessionDir(MAIN_CWD));
166
+ });
167
+ });
@@ -0,0 +1,293 @@
1
+ // src/__tests__/session-file-gc.test.ts
2
+ //
3
+ // 锁定 maybeCleanupExpiredSessionFiles 的 TTL 边界(数据安全风险):
4
+ // - 概率门(CLEANUP_PROBABILITY = 1/20)
5
+ // - TTL 边界(mtimeMs 恰好 = now - TTL_MS)
6
+ // - 只删 .jsonl,不删 .txt 等
7
+ // - 递归 walk 子目录
8
+ // - best-effort 吞错(只读目录不外抛)
9
+ //
10
+ // 边界算错会误删未过期 session 文件或不清理——数据安全风险。
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ import * as aliveStore from "../alive-store.ts";
18
+ import { maybeCleanupExpiredSessionFiles } from "../session-file-gc.ts";
19
+
20
+ // ============================================================
21
+ // helpers
22
+ // ============================================================
23
+
24
+ let tmpAgentDir: string;
25
+
26
+ beforeEach(() => {
27
+ tmpAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "subagents-gc-test-"));
28
+ });
29
+
30
+ afterEach(() => {
31
+ fs.rmSync(tmpAgentDir, { recursive: true, force: true });
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ /** 强制 Math.random 返回 0(< CLEANUP_PROBABILITY → 必触发清理)。 */
36
+ function forceCleanupTrigger(): void {
37
+ vi.spyOn(Math, "random").mockReturnValue(0);
38
+ }
39
+
40
+ /** 强制 Math.random 返回 1(>= CLEANUP_PROBABILITY → 跳过清理)。 */
41
+ function forceCleanupSkip(): void {
42
+ vi.spyOn(Math, "random").mockReturnValue(1);
43
+ }
44
+
45
+ /** 创建一个 .jsonl 文件,mtime 设为指定天数前。 */
46
+ function createSessionFile(relPath: string, daysAgo: number): string {
47
+ const fullPath = path.join(tmpAgentDir, "subagents", relPath);
48
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
49
+ fs.writeFileSync(fullPath, `{"id":"${relPath}"}\n`, "utf-8");
50
+ // 设置 mtime:daysAgo 天前
51
+ const targetTime = Date.now() / 1000 - daysAgo * 86400;
52
+ fs.utimesSync(fullPath, targetTime, targetTime);
53
+ return fullPath;
54
+ }
55
+
56
+ // ============================================================
57
+ // maybeCleanupExpiredSessionFiles
58
+ // ============================================================
59
+
60
+ describe("maybeCleanupExpiredSessionFiles", () => {
61
+ it("skips cleanup when random >= CLEANUP_PROBABILITY (probability gate)", () => {
62
+ forceCleanupSkip();
63
+ const oldFile = createSessionFile("old.jsonl", 31); // 31 天前,超 TTL
64
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
65
+ // 概率门跳过 → 文件仍在
66
+ expect(fs.existsSync(oldFile)).toBe(true);
67
+ });
68
+
69
+ it("triggers cleanup when random < CLEANUP_PROBABILITY", () => {
70
+ forceCleanupTrigger();
71
+ const oldFile = createSessionFile("old.jsonl", 31);
72
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
73
+ expect(fs.existsSync(oldFile)).toBe(false);
74
+ });
75
+
76
+ it("deletes files older than 30 days (TTL)", () => {
77
+ forceCleanupTrigger();
78
+ const old = createSessionFile("old.jsonl", 31);
79
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
80
+ expect(fs.existsSync(old)).toBe(false);
81
+ });
82
+
83
+ it("preserves files younger than 30 days", () => {
84
+ forceCleanupTrigger();
85
+ const young = createSessionFile("young.jsonl", 29);
86
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
87
+ expect(fs.existsSync(young)).toBe(true);
88
+ });
89
+
90
+ it("does NOT delete non-.jsonl files even if old (e.g. .txt)", () => {
91
+ forceCleanupTrigger();
92
+ const oldTxt = createSessionFile("old.txt", 31);
93
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
94
+ // .txt 不在清理范围
95
+ expect(fs.existsSync(oldTxt)).toBe(true);
96
+ });
97
+
98
+ it("recursively walks subdirectories", () => {
99
+ forceCleanupTrigger();
100
+ // 子目录中的旧文件
101
+ const oldNested = createSessionFile(
102
+ path.join("encoded-cwd-1", "sessions", "sess-old.jsonl"),
103
+ 31,
104
+ );
105
+ const youngNested = createSessionFile(
106
+ path.join("encoded-cwd-1", "sessions", "sess-young.jsonl"),
107
+ 5,
108
+ );
109
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
110
+ expect(fs.existsSync(oldNested)).toBe(false);
111
+ expect(fs.existsSync(youngNested)).toBe(true);
112
+ });
113
+
114
+ it("no-op when subagents dir does not exist (no throw)", () => {
115
+ forceCleanupTrigger();
116
+ // tmpAgentDir 下没有 subagents/ 目录
117
+ expect(() => maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd")).not.toThrow();
118
+ });
119
+
120
+ it("best-effort: never throws when 'subagents' is a file not a dir (readdirSync ENOTDIR)", () => {
121
+ // ESM 不允许 spyOn 命名空间导出,改用真实错误场景:
122
+ // 创建一个名为 subagents 的普通文件(非目录),existsSync 返 true 但 readdirSync 抛 ENOTDIR
123
+ forceCleanupTrigger();
124
+ const fileNotDir = path.join(tmpAgentDir, "subagents");
125
+ fs.writeFileSync(fileNotDir, "not a directory", "utf-8");
126
+ // walkAndClean 内部 readdirSync 抛错 → 被其 try/catch 吞掉 → 不外抛
127
+ expect(() => maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd")).not.toThrow();
128
+ });
129
+
130
+ it("TTL boundary: file just under 30 days is NOT deleted (strict greater-than semantics)", () => {
131
+ // walkAndClean 条件: now - stat.mtimeMs > TTL_MS(严格大于)
132
+ // 设 mtime 为 30 天前减去 2 分钟 → now - mtimeMs = TTL_MS - 2min + ε < TTL_MS → 保留
133
+ // 用 2 分钟缓冲消除「设 mtime 与 Date.now() 调用之间的几 ms 时间差」造成的抖动
134
+ forceCleanupTrigger();
135
+ const boundary = createSessionFile("boundary.jsonl", 0);
136
+ // 覆盖 mtime 为「30 天 - 2 分钟」前
137
+ const targetTime = Date.now() / 1000 - (30 * 86400 - 120);
138
+ fs.utimesSync(boundary, targetTime, targetTime);
139
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
140
+ // 严格 < TTL_MS → 不删(边界保留)
141
+ expect(fs.existsSync(boundary)).toBe(true);
142
+ });
143
+
144
+ it("handles empty subagents dir (no files, no throw)", () => {
145
+ forceCleanupTrigger();
146
+ // 创建空 subagents 目录
147
+ fs.mkdirSync(path.join(tmpAgentDir, "subagents"), { recursive: true });
148
+ expect(() => maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd")).not.toThrow();
149
+ });
150
+
151
+ // ---- .finalized sidecar 清理 ----
152
+
153
+ it("deletes .finalized sidecar along with expired .jsonl", () => {
154
+ forceCleanupTrigger();
155
+ const jsonl = createSessionFile("sess-finalized.jsonl", 31);
156
+ const finalized = `${jsonl}.finalized`;
157
+ fs.writeFileSync(finalized, "", "utf-8");
158
+ const targetTime = Date.now() / 1000 - 31 * 86400;
159
+ fs.utimesSync(finalized, targetTime, targetTime);
160
+
161
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
162
+
163
+ expect(fs.existsSync(jsonl)).toBe(false);
164
+ expect(fs.existsSync(finalized)).toBe(false);
165
+ });
166
+
167
+ it("cleans orphan .finalized sidecar when .jsonl already gone", () => {
168
+ forceCleanupTrigger();
169
+ const subagentsDir = path.join(tmpAgentDir, "subagents");
170
+ fs.mkdirSync(subagentsDir, { recursive: true });
171
+ const orphan = path.join(subagentsDir, "dead-session.jsonl.finalized");
172
+ fs.writeFileSync(orphan, "", "utf-8");
173
+ const targetTime = Date.now() / 1000 - 31 * 86400;
174
+ fs.utimesSync(orphan, targetTime, targetTime);
175
+
176
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
177
+
178
+ expect(fs.existsSync(orphan)).toBe(false);
179
+ });
180
+
181
+ // ---- .alive sidecar + pid 探活 ----
182
+
183
+ it("preserves expired .jsonl when .alive sidecar indicates live process (D-024)", () => {
184
+ forceCleanupTrigger();
185
+ const jsonl = createSessionFile("sess-alive.jsonl", 31);
186
+ const alivePath = `${jsonl}.alive`;
187
+ fs.writeFileSync(alivePath, JSON.stringify({ pid: 1, id: "s1", startedAt: Date.now() }), "utf-8");
188
+
189
+ vi.spyOn(aliveStore, "readAliveMarker").mockReturnValue({ pid: 1, id: "s1", startedAt: Date.now() });
190
+ vi.spyOn(aliveStore, "isProcessAlive").mockReturnValue(true);
191
+
192
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
193
+
194
+ expect(fs.existsSync(jsonl)).toBe(true);
195
+ expect(fs.existsSync(alivePath)).toBe(true);
196
+ });
197
+
198
+ it("deletes expired .jsonl + .alive when process is dead", () => {
199
+ forceCleanupTrigger();
200
+ const jsonl = createSessionFile("sess-dead.jsonl", 31);
201
+ const alivePath = `${jsonl}.alive`;
202
+ fs.writeFileSync(alivePath, JSON.stringify({ pid: 99999, id: "s2", startedAt: Date.now() }), "utf-8");
203
+
204
+ vi.spyOn(aliveStore, "readAliveMarker").mockReturnValue({ pid: 99999, id: "s2", startedAt: Date.now() });
205
+ vi.spyOn(aliveStore, "isProcessAlive").mockReturnValue(false);
206
+
207
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
208
+
209
+ expect(fs.existsSync(jsonl)).toBe(false);
210
+ expect(fs.existsSync(alivePath)).toBe(false);
211
+ });
212
+
213
+ it("cleans orphan .alive sidecar when .jsonl already gone", () => {
214
+ forceCleanupTrigger();
215
+ const subagentsDir = path.join(tmpAgentDir, "subagents");
216
+ fs.mkdirSync(subagentsDir, { recursive: true });
217
+ const orphan = path.join(subagentsDir, "dead-session.jsonl.alive");
218
+ fs.writeFileSync(orphan, JSON.stringify({ pid: 99999, id: "s3", startedAt: Date.now() }), "utf-8");
219
+ const targetTime = Date.now() / 1000 - 31 * 86400;
220
+ fs.utimesSync(orphan, targetTime, targetTime);
221
+
222
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
223
+
224
+ expect(fs.existsSync(orphan)).toBe(false);
225
+ });
226
+
227
+ it("deletes .jsonl + all sidecars (.cancelled, .finalized, .alive) together", () => {
228
+ forceCleanupTrigger();
229
+ const jsonl = createSessionFile("sess-all-sidecars.jsonl", 31);
230
+ const cancelled = `${jsonl}.cancelled`;
231
+ const finalized = `${jsonl}.finalized`;
232
+ const alive = `${jsonl}.alive`;
233
+ fs.writeFileSync(cancelled, "", "utf-8");
234
+ fs.writeFileSync(finalized, "", "utf-8");
235
+ fs.writeFileSync(alive, JSON.stringify({ pid: 99999, id: "s4", startedAt: Date.now() }), "utf-8");
236
+
237
+ vi.spyOn(aliveStore, "readAliveMarker").mockReturnValue({ pid: 99999, id: "s4", startedAt: Date.now() });
238
+ vi.spyOn(aliveStore, "isProcessAlive").mockReturnValue(false);
239
+
240
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
241
+
242
+ expect(fs.existsSync(jsonl)).toBe(false);
243
+ expect(fs.existsSync(cancelled)).toBe(false);
244
+ expect(fs.existsSync(finalized)).toBe(false);
245
+ expect(fs.existsSync(alive)).toBe(false);
246
+ });
247
+
248
+ // ---- [F2] manifest .json 清理(仅 records 子目录内)----
249
+ // records 目录布局:subagents/<enc>/records/<id>.json(D-004 cwd 物理隔离)。
250
+ // allowManifestJson 仅在名为 records 的子目录内打开,防止误删 enc 外层的
251
+ // worktrees.json(worktree reaper 依赖的状态文件)。
252
+
253
+ it("[F2] deletes expired manifest .json inside records/ subdir", () => {
254
+ forceCleanupTrigger();
255
+ const manifest = createSessionFile(
256
+ path.join("--Users-x-proj--", "records", "rec-1.json"),
257
+ 31,
258
+ );
259
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
260
+ expect(fs.existsSync(manifest)).toBe(false);
261
+ });
262
+
263
+ it("[F2] preserves manifest .json younger than TTL inside records/", () => {
264
+ forceCleanupTrigger();
265
+ const manifest = createSessionFile(
266
+ path.join("--Users-x-proj--", "records", "rec-young.json"),
267
+ 5,
268
+ );
269
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
270
+ expect(fs.existsSync(manifest)).toBe(true);
271
+ });
272
+
273
+ it("[F2] CRITICAL: does NOT delete worktrees.json at subagents/ root (regression)", () => {
274
+ // 破坏性风险点:worktrees.json 在 agentDir/subagents/worktrees.json(enc 外层第一层)。
275
+ // 暴力匹配所有 .json 会误删 → worktree reaper 失效。allowManifestJson 只在名为 records
276
+ // 的子目录内打开,根层的 worktrees.json 必须保留。
277
+ forceCleanupTrigger();
278
+ const worktrees = createSessionFile("worktrees.json", 31);
279
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
280
+ expect(fs.existsSync(worktrees)).toBe(true);
281
+ });
282
+
283
+ it("[F2] preserves .json.tmp.* inside records/ (recoverTmpFiles owns them)", () => {
284
+ // 跳过 .tmp.:session_start 的 recoverTmpFiles 同步处理 tmp,GC 不重复。
285
+ forceCleanupTrigger();
286
+ const tmp = createSessionFile(
287
+ path.join("--Users-x-proj--", "records", "rec-2.json.tmp.123"),
288
+ 31,
289
+ );
290
+ maybeCleanupExpiredSessionFiles(tmpAgentDir, "/cwd");
291
+ expect(fs.existsSync(tmp)).toBe(true);
292
+ });
293
+ });