@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,560 @@
1
+ // src/__tests__/record-store-index.test.ts
2
+ //
3
+ // [perf L-1] sessions-index.json(RecordStore 冷扫描索引)的模块级 + 集成级行为测试。
4
+ //
5
+ // 覆盖(S1TC1-13):
6
+ // S1TC1 C1 索引命中零探测(chmod 000 后新实例首扫仍返回全部记录)
7
+ // S1TC2 C2 戳不匹配单文件重探测(append 续聊 identity),其余零读取,条目更新
8
+ // S1TC3 C3 索引损坏回退全扫 + 重写自愈(无 console.error)
9
+ // S1TC4 C4 version:999(高于自身)整体忽略且不重写(内容与 mtime 均不变)
10
+ // S1TC4B C4b version:0(低于自身)整体丢弃,本轮 dirty 重写为当前版本
11
+ // S1TC5 C5 负条目命中——无 identity 的 junk 文件跨实例零重读(mtime 不变)
12
+ // S1TC6 C6 索引落 dirname(sessionsDir) 且写后 L0 快路径不失效
13
+ // S1TC7 C7 双实例顺序共享——A 落盘后 B 首次扫描零探测
14
+ // S1TC8 C8 落盘无 .tmp. 残留且产物可被下次加载完整消费
15
+ // S1TC9 节流:60s 最小间隔窗内不写、过窗后 dirty 才写
16
+ // S1TC10 模块级 loadIndex/saveIndex roundtrip
17
+ // S1TC11 模块级 loadIndex 三级校验链与版本分支
18
+ // S1TC12 模块级单条目字段损坏仅丢弃该条目
19
+ // S1TC13 dispose→revive 后重扫惰性重载索引命中
20
+ // S1TC14 索引命中 + .cancelled sidecar 冷启动——命中分支读取 tombstone 置 closed
21
+ //
22
+ // 异步落盘等待约定:断言「写完成」用 vi.waitFor(existsSync/内容)(rename 原子性保证
23
+ // 文件出现即完整);断言「未写」用 bounded settle(50ms 检测窗口——正确实现的写决策
24
+ // 在 collectRecords 同步段已被排除,窗口内不会有任何写发生);禁止固定长 sleep 等
25
+ // 待写完成。例外:S1TC9 fake Date 模式下 vi.waitFor 会推进 fake 时钟破坏时间线,
26
+ // 改用手写轮询 settleUntil(真实 setTimeout,仅计数限界)。
27
+ //
28
+ // 清理约定:真实 jsonl fixture 的 afterEach rmSync 必带 maxRetries(fire-and-forget
29
+ // 索引写与清理的 ENOTEMPTY 竞态,见 get-record-for-action-restart.test.ts)。
30
+ //
31
+ // 环境守卫约定:chmod 000 零探测用例(S1TC1/2/7/13)用 chmodProbeIt——win32 的
32
+ // chmod 000 仅映射 read-only、root 无视 000,断言会静默退化为恒真,须跳过。
33
+
34
+ import * as fs from "node:fs";
35
+ import * as os from "node:os";
36
+ import * as path from "node:path";
37
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
38
+
39
+ import { RecordStore } from "../execution/record-store";
40
+ import { INDEX_FILENAME, INDEX_VERSION, loadIndex, saveIndex } from "../execution/sessions-index";
41
+ import type { SessionsIndexEntry, SessionsIndexNegativeEntry } from "../execution/sessions-index";
42
+
43
+ /** chmod 000 零探测用例的环境守卫:win32 上 chmod 000 仅映射 read-only(读仍被允许)、
44
+ * root 无视 000——两种环境下「零内容读取」断言静默退化为恒真(实现退化回读也不失败),
45
+ * 跳过防假绿。平台短路在前,getuid 仅 POSIX 存在。 */
46
+ const chmodProbeIt = process.platform === "win32" || process.getuid?.() === 0 ? it.skip : it;
47
+
48
+ /** bounded 轮询:真实 setTimeout(20ms/轮),rounds 上限防挂死。fake Date 模式专用
49
+ * (vi.waitFor 在 fake timers 下会 advanceTimersByTime 推进 fake Date,破坏节流时间线)。
50
+ * 预算 10s:全量并行跑时机器高负载,fire-and-forget 落盘链(含 fsync)可能远慢于平时。 */
51
+ async function settleUntil(cond: () => boolean, rounds = 500): Promise<void> {
52
+ for (let i = 0; i < rounds; i++) {
53
+ if (cond()) return;
54
+ await new Promise((r) => setTimeout(r, 20));
55
+ }
56
+ if (!cond()) throw new Error("settleUntil: condition not met within bounded rounds");
57
+ }
58
+
59
+ // ============================================================
60
+ // 模块级(不经 RecordStore)
61
+ // ============================================================
62
+
63
+ describe("sessions-index 模块(S1TC10-12)", () => {
64
+ let encDir: string;
65
+ let indexPath: string;
66
+
67
+ beforeEach(() => {
68
+ encDir = fs.mkdtempSync(path.join(os.tmpdir(), "subagents-index-mod-"));
69
+ indexPath = path.join(encDir, INDEX_FILENAME);
70
+ });
71
+
72
+ afterEach(() => {
73
+ fs.rmSync(encDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
74
+ });
75
+
76
+ /** 合法正条目模板(字段值任意但类型齐全)。 */
77
+ function positiveEntry(over: Partial<SessionsIndexEntry> = {}): SessionsIndexEntry {
78
+ return {
79
+ mtimeMs: 111,
80
+ size: 222,
81
+ id: "sa-1",
82
+ agent: "worker",
83
+ mode: "background",
84
+ task: "test task",
85
+ slug: "test",
86
+ startedAt: 1000,
87
+ rootSessionId: "root-1",
88
+ parentRecordId: undefined,
89
+ depth: 0,
90
+ model: "provider/model",
91
+ thinkingLevel: undefined,
92
+ ...over,
93
+ };
94
+ }
95
+
96
+ it("S1TC10: loadIndex/saveIndex roundtrip——正/负条目与 model 空串完整往返、无 .tmp. 残留", async () => {
97
+ const entries = new Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry>([
98
+ // model 空串:尾部探测(readIdentityTail)拿不到 model_change 的合法结果,不当损坏(DS4)
99
+ ["a.jsonl", positiveEntry({ model: "" })],
100
+ ["junk.jsonl", { negative: true, mtimeMs: 1, size: 2 }],
101
+ // undefined 可选字段:JSON 序列化丢弃,往返保持 undefined
102
+ ["b.jsonl", positiveEntry({ id: "sa-2", rootSessionId: undefined, thinkingLevel: undefined })],
103
+ ]);
104
+ await saveIndex(encDir, { entries });
105
+
106
+ const loaded = loadIndex(encDir);
107
+ expect(loaded.higherVersion).toBe(false);
108
+ expect(loaded.entries.size).toBe(3);
109
+ expect(loaded.entries.get("a.jsonl")).toEqual(entries.get("a.jsonl"));
110
+ expect(loaded.entries.get("junk.jsonl")).toEqual({ negative: true, mtimeMs: 1, size: 2 });
111
+ expect(loaded.entries.get("b.jsonl")).toEqual(entries.get("b.jsonl"));
112
+
113
+ // 正常路径无 .tmp. 残留(rename 成功消费 tmp)
114
+ expect(fs.readdirSync(encDir)).toEqual([INDEX_FILENAME]);
115
+ });
116
+
117
+ it("S1TC11: loadIndex 三级校验链与版本分支——永不抛,高版本整体忽略", () => {
118
+ // ① 空 encDir(无文件)② 非法 JSON ③ 顶层结构不符(entries 数组 / version 缺失)
119
+ // ⑤ version 低于自身 → 均为 {entries: 空, higherVersion: false}
120
+ expect(loadIndex(encDir)).toEqual({ entries: new Map(), higherVersion: false }); // ①
121
+
122
+ fs.writeFileSync(indexPath, "{{{"); // ②
123
+ expect(loadIndex(encDir).entries.size).toBe(0);
124
+ expect(loadIndex(encDir).higherVersion).toBe(false);
125
+
126
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 1, pid: 1, entries: [1, 2] })); // ③a 数组非 object
127
+ expect(loadIndex(encDir).entries.size).toBe(0);
128
+ expect(loadIndex(encDir).higherVersion).toBe(false);
129
+
130
+ fs.writeFileSync(indexPath, JSON.stringify({ pid: 1, entries: {} })); // ③b version 缺失
131
+ expect(loadIndex(encDir).entries.size).toBe(0);
132
+ expect(loadIndex(encDir).higherVersion).toBe(false);
133
+
134
+ // ④ version:999(高于自身):空 Map + higherVersion:true——即使 entries 合法也不消费
135
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 999, pid: 1, entries: { "a.jsonl": positiveEntry() } }));
136
+ const higher = loadIndex(encDir);
137
+ expect(higher.entries.size).toBe(0);
138
+ expect(higher.higherVersion).toBe(true);
139
+
140
+ // ⑤ version:0(低于自身):整体丢弃 → 空索引可写(下轮 dirty 重写自愈)
141
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 0, pid: 1, entries: { "a.jsonl": positiveEntry() } }));
142
+ expect(loadIndex(encDir)).toEqual({ entries: new Map(), higherVersion: false });
143
+ });
144
+
145
+ it("S1TC12: 单条目字段损坏仅丢弃该条目,其余条目正常保留", () => {
146
+ const good = positiveEntry();
147
+ const raw: Record<string, unknown> = {
148
+ good,
149
+ badTask: { ...good, task: 123 }, // task 为 number
150
+ badMode: { ...good, mode: "async" }, // mode 越界
151
+ noStamp: { ...good, mtimeMs: undefined, size: undefined }, // 缺戳字段(JSON 化后缺失)
152
+ badNeg: { negative: true, mtimeMs: 1 }, // 负条目缺 size
153
+ };
154
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 1, pid: 1, entries: raw }));
155
+
156
+ const loaded = loadIndex(encDir);
157
+ expect(loaded.entries.size).toBe(1); // 单条目损坏不放大为整体失效
158
+ expect(loaded.entries.get("good")).toEqual(good);
159
+ });
160
+ });
161
+
162
+ // ============================================================
163
+ // 集成级(经 RecordStore)
164
+ // ============================================================
165
+
166
+ describe("RecordStore 索引接入 [perf L-1](S1TC1-9/13)", () => {
167
+ let rootDir: string;
168
+ let sessionsDir: string;
169
+ let encDir: string;
170
+ let indexPath: string;
171
+
172
+ beforeEach(() => {
173
+ // 两层布局(对齐生产 <enc> 段结构):索引落 dirname(sessionsDir)=rootDir
174
+ rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "subagents-index-"));
175
+ sessionsDir = path.join(rootDir, "sessions");
176
+ fs.mkdirSync(sessionsDir);
177
+ encDir = path.dirname(sessionsDir);
178
+ indexPath = path.join(encDir, INDEX_FILENAME);
179
+ });
180
+
181
+ afterEach(() => {
182
+ // 恢复权限以防 tmp 清理失败(chmod 000 用例会留下不可读文件/目录)
183
+ try { fs.chmodSync(sessionsDir, 0o755); } catch { /* best-effort */ }
184
+ for (const f of fs.readdirSync(sessionsDir)) {
185
+ try { fs.chmodSync(path.join(sessionsDir, f), 0o644); } catch { /* best-effort */ }
186
+ }
187
+ fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
188
+ });
189
+
190
+ /** 构造一个 session.jsonl(header + identity + N 条 assistant message)。复刻
191
+ * record-store-cache.test.ts 的 writeSession 形态。 */
192
+ function writeSession(opts: {
193
+ name: string;
194
+ id: string;
195
+ agent?: string;
196
+ startedAt?: number;
197
+ rootSessionId?: string;
198
+ assistantTexts?: string[];
199
+ }): string {
200
+ const file = path.join(sessionsDir, opts.name);
201
+ const lines: unknown[] = [
202
+ { type: "session", id: "sess" },
203
+ {
204
+ type: "custom",
205
+ customType: "subagent-identity",
206
+ data: {
207
+ id: opts.id,
208
+ agent: opts.agent ?? "worker",
209
+ mode: "background",
210
+ task: "test task",
211
+ slug: "test",
212
+ startedAt: opts.startedAt ?? 1000,
213
+ rootSessionId: opts.rootSessionId ?? "root-1",
214
+ depth: 0,
215
+ },
216
+ },
217
+ ];
218
+ for (const text of opts.assistantTexts ?? []) {
219
+ lines.push({
220
+ type: "message",
221
+ timestamp: "2026-08-14T00:00:00.000Z",
222
+ message: {
223
+ role: "assistant",
224
+ content: [{ type: "text", text }],
225
+ usage: { input: 10, output: 5 },
226
+ stopReason: "stop",
227
+ timestamp: 2000,
228
+ },
229
+ });
230
+ }
231
+ fs.writeFileSync(file, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
232
+ return file;
233
+ }
234
+
235
+ /** 续聊形态 session:头部 >64KB padding(identity 被推出头部窗口),identity 在尾部附近。
236
+ * 复刻 record-store-cache.test.ts B5 用例的构造(readIdentityTail 主力命中形态)。 */
237
+ function writeTailIdentitySession(name: string, id: string, task: string): string {
238
+ const file = path.join(sessionsDir, name);
239
+ const lines = [
240
+ JSON.stringify({ type: "session", id: "sess" }),
241
+ JSON.stringify({ type: "message", message: { role: "user", content: "x".repeat(70000), timestamp: 1000 } }),
242
+ JSON.stringify({
243
+ type: "custom",
244
+ customType: "subagent-identity",
245
+ data: { id, agent: "worker", mode: "background", task, slug: "tail", startedAt: 1000, rootSessionId: "root-1", depth: 0 },
246
+ }),
247
+ JSON.stringify({
248
+ type: "message",
249
+ timestamp: "2026-08-14T00:00:00.000Z",
250
+ message: { role: "assistant", content: [{ type: "text", text: "out" }], usage: { input: 1, output: 1 }, stopReason: "stop", timestamp: 2000 },
251
+ }),
252
+ ];
253
+ fs.writeFileSync(file, lines.join("\n") + "\n");
254
+ return file;
255
+ }
256
+
257
+ /** 无 identity 的 junk 文件(仅 session header)。复刻 record-store-cache.test.ts 构造。 */
258
+ function writeJunk(name: string): string {
259
+ const file = path.join(sessionsDir, name);
260
+ fs.writeFileSync(file, JSON.stringify({ type: "session", id: "sess2" }) + "\n");
261
+ return file;
262
+ }
263
+
264
+ /** 等待索引文件落盘(rename 原子性保证出现即完整)。 */
265
+ async function waitForIndex(): Promise<void> {
266
+ await vi.waitFor(() => {
267
+ if (!fs.existsSync(indexPath)) throw new Error("sessions-index not written yet");
268
+ }, { timeout: 8_000 });
269
+ }
270
+
271
+ chmodProbeIt("S1TC1: 索引命中零探测——chmod 000 后新 RecordStore 首次 collectRecords 仍返回全部记录", { timeout: 15_000 }, async () => {
272
+ const f1 = writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
273
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
274
+
275
+ const storeA = new RecordStore(sessionsDir);
276
+ storeA.collectRecords(100, "all", "root-1");
277
+ await waitForIndex();
278
+
279
+ // 去读权限:statSync 仍可用(戳 mtimeMs/size 不变 → 索引条目仍命中),readFileSync/
280
+ // openSync 失败——若实现退化回读文件,readIdentityHeader 捕获异常返回 undefined → sa-1 消失。
281
+ fs.chmodSync(f1, 0o000);
282
+ const storeB = new RecordStore(sessionsDir);
283
+ const records = storeB.collectRecords(100, "all", "root-1");
284
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]);
285
+ const sa1 = records.find((r) => r.id === "sa-1");
286
+ expect(sa1?.agent).toBe("worker");
287
+ expect(sa1?.task).toBe("test task");
288
+ expect(sa1?.startedAt).toBe(1000);
289
+ fs.chmodSync(f1, 0o644);
290
+ });
291
+
292
+ chmodProbeIt("S1TC2: 戳不匹配单文件重探测——append 后仅该文件重探测、其余零读取、索引条目更新", { timeout: 15_000 }, async () => {
293
+ // a.jsonl 续聊形态(identity 在尾部):append 新 identity 后 readIdentityTail 重探测
294
+ // 取最后一条 → task 更新为 "resumed"(若从头部取第一条 identity 则拿到旧值,断言失败)。
295
+ const fA = writeTailIdentitySession("a.jsonl", "sa-1", "initial");
296
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
297
+
298
+ const storeA = new RecordStore(sessionsDir);
299
+ storeA.collectRecords(100, "all", "root-1");
300
+ await waitForIndex();
301
+
302
+ // append 新 subagent-identity entry(模拟续聊,size 必变 → 索引戳不匹配)
303
+ fs.appendFileSync(
304
+ fA,
305
+ JSON.stringify({
306
+ type: "custom",
307
+ customType: "subagent-identity",
308
+ data: { id: "sa-1", agent: "worker", mode: "background", task: "resumed", slug: "tail", startedAt: 1000, rootSessionId: "root-1", depth: 0 },
309
+ }) + "\n",
310
+ );
311
+ // b.jsonl 戳未变 → 应索引命中零读取(退化回读则消失)
312
+ fs.chmodSync(path.join(sessionsDir, "b.jsonl"), 0o000);
313
+
314
+ const bBefore = loadIndex(encDir).entries.get("b.jsonl");
315
+ const storeB = new RecordStore(sessionsDir);
316
+ const records = storeB.collectRecords(100, "all", "root-1");
317
+ expect(records.find((r) => r.id === "sa-1")?.task).toBe("resumed"); // a 重探测取尾部 identity
318
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]); // b 索引命中仍在
319
+
320
+ // 探测结果写回索引:a 条目戳对齐当前 stat 且 task="resumed";b 条目未被触碰
321
+ await vi.waitFor(() => {
322
+ const e = loadIndex(encDir).entries.get("a.jsonl");
323
+ if (e === undefined || e.negative === true || e.task !== "resumed") throw new Error("a entry not rewritten yet");
324
+ }, { timeout: 8_000 });
325
+ const aEntry = loadIndex(encDir).entries.get("a.jsonl");
326
+ const aStat = fs.statSync(fA);
327
+ expect(aEntry?.mtimeMs).toBe(aStat.mtimeMs);
328
+ expect(aEntry?.size).toBe(aStat.size);
329
+ expect(loadIndex(encDir).entries.get("b.jsonl")).toEqual(bBefore); // 单文件失效不波及邻居
330
+ });
331
+
332
+ it("S1TC3: 索引损坏回退全扫 + 重写自愈", { timeout: 15_000 }, async () => {
333
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
334
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
335
+ fs.writeFileSync(indexPath, "{not valid json");
336
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
337
+
338
+ const store = new RecordStore(sessionsDir);
339
+ const records = store.collectRecords(100, "all", "root-1");
340
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]); // 空索引=今天的全量行为
341
+
342
+ await vi.waitFor(() => {
343
+ JSON.parse(fs.readFileSync(indexPath, "utf-8")); // 轮询索引变合法(损坏→空索引→全扫 dirty→重写自愈)
344
+ }, { timeout: 8_000 });
345
+ const parsed = JSON.parse(fs.readFileSync(indexPath, "utf-8")) as { version?: number; entries: Record<string, unknown> };
346
+ expect(parsed.version).toBe(INDEX_VERSION);
347
+ expect(Object.keys(parsed.entries).sort()).toEqual(["a.jsonl", "b.jsonl"]);
348
+ expect(errSpy).not.toHaveBeenCalled(); // 损坏走 debug 日志(PI_EXT_DEBUG=1 可见),不 console.error
349
+ errSpy.mockRestore();
350
+ });
351
+
352
+ it("S1TC4: version:999(高于自身)整体忽略且不重写——内容与 mtime 均不变", async () => {
353
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
354
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
355
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 999, pid: 1, entries: {} }));
356
+ const before = { content: fs.readFileSync(indexPath, "utf-8"), mtimeMs: fs.statSync(indexPath).mtimeMs };
357
+
358
+ const store = new RecordStore(sessionsDir);
359
+ const records = store.collectRecords(100, "all", "root-1");
360
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]); // 空索引全量探测,结果正确
361
+
362
+ await new Promise((r) => setTimeout(r, 50)); // bounded settle:正确实现从 collectRecords 返回起就永不发起写
363
+ expect(fs.readFileSync(indexPath, "utf-8")).toBe(before.content); // 磁盘内容逐字节不动
364
+ expect(fs.statSync(indexPath).mtimeMs).toBe(before.mtimeMs); // mtime 不动(高版本只忽略不重写)
365
+ });
366
+
367
+ it("S1TC4B: version:0(低于自身)整体丢弃,本轮 dirty 重写为 v1", { timeout: 15_000 }, async () => {
368
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
369
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
370
+ fs.writeFileSync(indexPath, JSON.stringify({ version: 0, pid: 1, entries: {} }));
371
+
372
+ const store = new RecordStore(sessionsDir);
373
+ const records = store.collectRecords(100, "all", "root-1");
374
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]);
375
+
376
+ await vi.waitFor(() => {
377
+ const parsed = JSON.parse(fs.readFileSync(indexPath, "utf-8")) as { version?: number };
378
+ if (parsed.version !== INDEX_VERSION) throw new Error("index not rewritten to current version yet");
379
+ }, { timeout: 8_000 });
380
+ const parsed = JSON.parse(fs.readFileSync(indexPath, "utf-8")) as { entries: Record<string, unknown> };
381
+ expect(Object.keys(parsed.entries).sort()).toEqual(["a.jsonl", "b.jsonl"]); // 低版本丢弃→全扫 dirty→重写自愈
382
+ });
383
+
384
+ it("S1TC5: 负条目命中——无 identity 的 junk 文件跨实例零重读", { timeout: 15_000 }, async () => {
385
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
386
+ writeJunk("junk.jsonl");
387
+
388
+ const storeA = new RecordStore(sessionsDir);
389
+ storeA.collectRecords(100, "all", "root-1");
390
+ await waitForIndex();
391
+
392
+ const negEntry = loadIndex(encDir).entries.get("junk.jsonl");
393
+ expect(negEntry).toEqual({ negative: true, mtimeMs: expect.any(Number), size: expect.any(Number) }); // 负缓存持久化
394
+
395
+ const before = fs.statSync(indexPath).mtimeMs;
396
+ const storeB = new RecordStore(sessionsDir);
397
+ const records = storeB.collectRecords(100, "all", "root-1");
398
+ expect(records.map((r) => r.id)).toEqual(["sa-1"]); // junk 无 identity 本就不产记录;a 命中在
399
+
400
+ await new Promise((r) => setTimeout(r, 50));
401
+ expect(fs.statSync(indexPath).mtimeMs).toBe(before); // B 全命中零探测→不 dirty→不写(负条目命中失效则 junk 重探测→dirty→重写→mtime 变)
402
+ });
403
+
404
+ it("S1TC6: 索引落 dirname(sessionsDir) 且写后 L0 快路径不失效", { timeout: 15_000 }, async () => {
405
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
406
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
407
+ const store = new RecordStore(sessionsDir); // 不挂 manifestStore:排除 listAllSync 的 readdir 干扰
408
+
409
+ const first = store.collectRecords(100, "all", "root-1");
410
+ await waitForIndex();
411
+
412
+ // 兄弟位置三佐证之 readdir 过滤:索引在 root(dirname(sessionsDir))不在 sessionsDir 内部
413
+ expect(fs.existsSync(indexPath)).toBe(true);
414
+ expect(fs.readdirSync(sessionsDir)).not.toContain(INDEX_FILENAME);
415
+
416
+ // L0 快路径观测:chmod 000 sessionsDir 目录——statSync 仍可用(目录 mtime 不变 → 快路径
417
+ // 判定成立),readdirSync EACCES 失败(慢路径 return [] → 0 条)。二次 collectRecords 仍
418
+ // 返回全部记录 = readdir 被跳过 = 索引写入未改 sessionsDir 目录 mtime。
419
+ // (原设计 spyOn(fs,"readdirSync") 在 ESM 模块命名空间不可配置,改用等价行为观测。)
420
+ fs.chmodSync(sessionsDir, 0o000);
421
+ try {
422
+ const second = store.collectRecords(100, "all", "root-1");
423
+ expect(second.map((r) => r.id).sort()).toEqual(first.map((r) => r.id).sort());
424
+ expect(second).toHaveLength(2);
425
+ } finally {
426
+ fs.chmodSync(sessionsDir, 0o755);
427
+ }
428
+ });
429
+
430
+ chmodProbeIt("S1TC7: 双实例顺序共享——A 落盘后 B 首次扫描零探测", { timeout: 15_000 }, async () => {
431
+ const fa = writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
432
+ const fb = writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
433
+ const fj = writeJunk("junk.jsonl");
434
+
435
+ const storeA = new RecordStore(sessionsDir);
436
+ const recordsA = storeA.collectRecords(100, "all", "root-1");
437
+ await waitForIndex();
438
+
439
+ fs.chmodSync(fa, 0o000);
440
+ fs.chmodSync(fb, 0o000);
441
+ fs.chmodSync(fj, 0o000);
442
+ const before = fs.statSync(indexPath).mtimeMs;
443
+
444
+ const storeB = new RecordStore(sessionsDir);
445
+ const recordsB = storeB.collectRecords(100, "all", "root-1");
446
+ const project = (rs: typeof recordsA) =>
447
+ rs.map((r) => `${r.id}|${r.agent}|${r.task}|${r.startedAt}|${r.rootSessionId}|${r.status}`).sort();
448
+ expect(project(recordsB)).toEqual(project(recordsA)); // 正+负条目全命中零读取
449
+
450
+ await new Promise((r) => setTimeout(r, 50));
451
+ expect(fs.statSync(indexPath).mtimeMs).toBe(before); // B 零探测未发起写(顺序共享语义)
452
+ });
453
+
454
+ it("S1TC8: 落盘无 .tmp. 残留且产物可被下次加载完整消费", { timeout: 15_000 }, async () => {
455
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
456
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
457
+ writeJunk("junk.jsonl");
458
+
459
+ const storeA = new RecordStore(sessionsDir);
460
+ const recordsA = storeA.collectRecords(100, "all", "root-1");
461
+ await waitForIndex();
462
+
463
+ const files = fs.readdirSync(encDir);
464
+ expect(files.some((f) => f.includes(".tmp."))).toBe(false); // 正常路径 rename 成功消费 tmp
465
+
466
+ const loaded = loadIndex(encDir);
467
+ expect(loaded.higherVersion).toBe(false);
468
+ expect(loaded.entries.size).toBe(3); // 2 正 + 1 负,快照完整
469
+ const aEntry = loaded.entries.get("a.jsonl");
470
+ if (aEntry === undefined || aEntry.negative === true) throw new Error("a.jsonl should be a positive entry");
471
+ expect(aEntry.id).toBe(recordsA.find((r) => r.id === "sa-1")?.id);
472
+ expect(aEntry.task).toBe("test task");
473
+ expect(aEntry.model).toBe(""); // 头部探测拿不到 model_change → 空串(DS4 合法)
474
+ });
475
+
476
+ it("S1TC9: 节流——60s 最小间隔窗内不写、过窗后 dirty 才写", { timeout: 30_000 }, async () => {
477
+ // 仅 fake Date(setTimeout/轮询走真实时钟不冲突);RecordStore 节流判定与
478
+ // lastIndexWriteAt 全走 Date.now()(先例 worktree-pid-registration.integration.test.ts)。
479
+ vi.useFakeTimers({ toFake: ["Date"] });
480
+ const T0 = Date.now();
481
+ vi.setSystemTime(T0);
482
+ try {
483
+ const store = new RecordStore(sessionsDir);
484
+
485
+ // 第一段:首扫必写(lastIndexWriteAt=0 过节流窗),索引含 a
486
+ writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
487
+ store.collectRecords(100, "all", "root-1");
488
+ // settle 必须等「节流打戳完成」而非仅「文件出现」:打戳在 saveIndex 成功 .then 里
489
+ // 异步推进(rename → 目录 fsync 的 open/sync/close → resolve 之后),高负载下可能
490
+ // 晚于文件出现。若先推进 fake 时钟,打戳读到推进后的 Date.now()(T0+30s)→ 节流
491
+ // 基准被污染 → 第三段永判窗内 → settle 超时(全量并行偶发失败的根因)。打戳无
492
+ // 公共 API 可观测,轮询软私有字段本身(Reflect.get + typeof guard,免类型断言);
493
+ // 比固定空转轮数(目录 fsync 的 IO 完成时间无上界)这是确定性等待。
494
+ await settleUntil(() => {
495
+ const stamped = Reflect.get(store, "lastIndexWriteAt");
496
+ return fs.existsSync(indexPath) && typeof stamped === "number" && stamped > 0;
497
+ });
498
+ expect(loadIndex(encDir).entries.get("a.jsonl")).toBeDefined();
499
+
500
+ // 第二段:T0+30s(距成功写 30s < 60s)新文件 b → 慢路径 → b 探测 dirty,但窗内不写
501
+ writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
502
+ vi.setSystemTime(T0 + 30_000);
503
+ store.collectRecords(100, "all", "root-1");
504
+ await settleUntil(() => loadIndex(encDir).entries.get("a.jsonl") !== undefined); // 稳定基线
505
+ expect(loadIndex(encDir).entries.get("b.jsonl")).toBeUndefined(); // 节流窗内 dirty 扫描不落盘
506
+
507
+ // 第三段:T0+61s(过窗)新文件 c → dirty → 写入全量快照(含 b 与 c)
508
+ writeSession({ name: "c.jsonl", id: "sa-3", assistantTexts: ["r3"] });
509
+ vi.setSystemTime(T0 + 61_000);
510
+ store.collectRecords(100, "all", "root-1");
511
+ await settleUntil(() => {
512
+ const e = loadIndex(encDir).entries;
513
+ return e.get("b.jsonl") !== undefined && e.get("c.jsonl") !== undefined;
514
+ });
515
+ expect(loadIndex(encDir).entries.get("a.jsonl")).toBeDefined(); // 全量快照含全部最新探测结果
516
+ } finally {
517
+ vi.useRealTimers();
518
+ }
519
+ });
520
+
521
+ chmodProbeIt("S1TC13: dispose→revive 后重扫惰性重载索引命中(/resume /fork 高频路径)", { timeout: 15_000 }, async () => {
522
+ const fa = writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
523
+ const fb = writeSession({ name: "b.jsonl", id: "sa-2", assistantTexts: ["r2"] });
524
+ const store = new RecordStore(sessionsDir);
525
+ store.collectRecords(100, "all", "root-1");
526
+ await waitForIndex();
527
+
528
+ store.dispose(); // 清 fileCache/dirStamp/索引字段重置为初始值
529
+ store.revive(); // 不预载索引:revive 后 dirStamp===null 重扫时惰性加载
530
+
531
+ fs.chmodSync(fa, 0o000);
532
+ fs.chmodSync(fb, 0o000);
533
+ const records = store.collectRecords(100, "all", "root-1");
534
+ expect(records.map((r) => r.id).sort()).toEqual(["sa-1", "sa-2"]); // 重扫 → loadIndex 重载 → 正条目命中零内容读取
535
+ });
536
+
537
+ it("S1TC14: 索引命中 + .cancelled sidecar 冷启动——命中分支读取 tombstone 置 closed", { timeout: 15_000 }, async () => {
538
+ const f1 = writeSession({ name: "a.jsonl", id: "sa-1", assistantTexts: ["r1"] });
539
+
540
+ const storeA = new RecordStore(sessionsDir);
541
+ storeA.collectRecords(100, "all", "root-1");
542
+ await waitForIndex();
543
+
544
+ // jsonl 戳不变,仅在旁路放 .cancelled sidecar(cancel 后 jsonl 截断、状态只能靠
545
+ // tombstone 重建;sidecar 形态复刻 record-store-cache.test.ts A3 用例)
546
+ fs.writeFileSync(
547
+ `${f1}.cancelled`,
548
+ JSON.stringify({ id: "sa-1", status: "cancelled", agent: "worker", startedAt: 1000, endedAt: 3000 }) + "\n",
549
+ );
550
+
551
+ // 冷启动新实例:jsonl 戳命中索引条目 → 走命中分支,但 cancelled sidecar stat 变化
552
+ // 必须被感知——命中分支读 tombstone override 状态(否则退回兜底 running)
553
+ const storeB = new RecordStore(sessionsDir);
554
+ const sa1 = storeB.collectRecords(100, "all", "root-1").find((r) => r.id === "sa-1");
555
+ expect(sa1?.status).toBe("closed");
556
+ expect(sa1?.closedReason).toBe("cancelled");
557
+ expect(sa1?.error).toBe("cancelled by user");
558
+ expect(sa1?.endedAt).toBe(3000); // tombstone 的精确结束时间,非 mtime 近似
559
+ });
560
+ });