@zhushanwen/pi-subagent-workflow 8.5.0 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/package.json +13 -16
  2. package/relay/relay.mjs +1 -1
  3. package/src/host/__tests__/pi-host.test.ts +214 -0
  4. package/src/host/pi-host.ts +109 -0
  5. package/src/index.ts +142 -32
  6. package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
  7. package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
  8. package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
  9. package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
  10. package/src/injectors/engine-awareness.ts +123 -0
  11. package/src/injectors/model-list-injector.ts +1 -1
  12. package/src/injectors/subagent-list-injector.ts +8 -8
  13. package/src/injectors/workflow-list-injector.ts +8 -8
  14. package/src/interface/__tests__/detectors.test.ts +3 -3
  15. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  16. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  17. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
  18. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  19. package/src/interface/bg-notify-render.ts +34 -13
  20. package/src/interface/commands.ts +3 -3
  21. package/src/interface/format.ts +2 -2
  22. package/src/interface/helpers.ts +3 -3
  23. package/src/interface/list-component.ts +4 -4
  24. package/src/interface/list-shared.ts +2 -2
  25. package/src/interface/list-view.ts +2 -2
  26. package/src/interface/subagent-actions.ts +288 -19
  27. package/src/interface/subagent-tool-schema.ts +163 -0
  28. package/src/interface/subagent-tool.ts +65 -129
  29. package/src/interface/subagents.ts +5 -5
  30. package/src/interface/tool-render.ts +34 -2
  31. package/src/interface/tool-workflow-script.ts +4 -4
  32. package/src/interface/tool-workflow.ts +6 -6
  33. package/src/interface/views/WorkflowsView.ts +5 -5
  34. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  35. package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
  36. package/src/interface/views/detail-content.ts +4 -4
  37. package/src/interface/views/format.ts +4 -4
  38. package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
  39. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
  40. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
  41. package/src/execution/__tests__/agent-registry.test.ts +0 -363
  42. package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
  43. package/src/execution/__tests__/alive-store.test.ts +0 -147
  44. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  45. package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
  46. package/src/execution/__tests__/bg-notify-render.test.ts +0 -256
  47. package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
  48. package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
  49. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
  50. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
  51. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
  52. package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
  53. package/src/execution/__tests__/config.test.ts +0 -110
  54. package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
  55. package/src/execution/__tests__/crash-recovery.test.ts +0 -320
  56. package/src/execution/__tests__/delivery-methods.test.ts +0 -385
  57. package/src/execution/__tests__/dialog-queue.test.ts +0 -299
  58. package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
  59. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
  60. package/src/execution/__tests__/execute-nesting.test.ts +0 -323
  61. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -189
  62. package/src/execution/__tests__/execution-record.test.ts +0 -1284
  63. package/src/execution/__tests__/finalize-record.test.ts +0 -368
  64. package/src/execution/__tests__/finalized-marker.test.ts +0 -82
  65. package/src/execution/__tests__/format-schema-instruction.test.ts +0 -138
  66. package/src/execution/__tests__/format.test.ts +0 -458
  67. package/src/execution/__tests__/gc-timer.test.ts +0 -184
  68. package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
  69. package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
  70. package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
  71. package/src/execution/__tests__/helpers/spawn-mock.ts +0 -238
  72. package/src/execution/__tests__/host-mode.test.ts +0 -87
  73. package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
  74. package/src/execution/__tests__/index-session-start.test.ts +0 -593
  75. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  76. package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
  77. package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
  78. package/src/execution/__tests__/list-component.test.ts +0 -401
  79. package/src/execution/__tests__/list-fields.test.ts +0 -109
  80. package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
  81. package/src/execution/__tests__/model-resolver.test.ts +0 -413
  82. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
  83. package/src/execution/__tests__/nested-visibility.test.ts +0 -325
  84. package/src/execution/__tests__/notifier-flush.test.ts +0 -574
  85. package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
  86. package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
  87. package/src/execution/__tests__/output-collector.test.ts +0 -61
  88. package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
  89. package/src/execution/__tests__/path-encoding.test.ts +0 -104
  90. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  91. package/src/execution/__tests__/record-store.test.ts +0 -1057
  92. package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
  93. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
  94. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
  95. package/src/execution/__tests__/relay-agent.test.ts +0 -448
  96. package/src/execution/__tests__/relay-env.test.ts +0 -42
  97. package/src/execution/__tests__/resource-policy.test.ts +0 -109
  98. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  99. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
  100. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
  101. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -644
  102. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  103. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  104. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  105. package/src/execution/__tests__/sdk-contract.test.ts +0 -273
  106. package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
  107. package/src/execution/__tests__/session-file-gc.test.ts +0 -293
  108. package/src/execution/__tests__/session-pending.test.ts +0 -197
  109. package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
  110. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
  111. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -311
  112. package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
  113. package/src/execution/__tests__/spawn-args.test.ts +0 -434
  114. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
  115. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  116. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
  117. package/src/execution/__tests__/spawned-children.test.ts +0 -92
  118. package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
  119. package/src/execution/__tests__/status-refactor.test.ts +0 -345
  120. package/src/execution/__tests__/stdin-writer.test.ts +0 -463
  121. package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
  122. package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
  123. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
  124. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
  125. package/src/execution/__tests__/subagent-service.test.ts +0 -788
  126. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
  127. package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -506
  128. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  129. package/src/execution/__tests__/timeout-integration.test.ts +0 -397
  130. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  131. package/src/execution/__tests__/tool-action.test.ts +0 -385
  132. package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
  133. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  134. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  135. package/src/execution/__tests__/ui-channels.test.ts +0 -187
  136. package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
  137. package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
  138. package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
  139. package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
  140. package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
  141. package/src/execution/__tests__/worktree-manager.test.ts +0 -633
  142. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
  143. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
  144. package/src/execution/__tests__/worktree-registry.test.ts +0 -199
  145. package/src/execution/agent-registry.ts +0 -193
  146. package/src/execution/agent-result-mapper.ts +0 -90
  147. package/src/execution/alive-store.ts +0 -92
  148. package/src/execution/argv-mirror.ts +0 -109
  149. package/src/execution/best-effort.ts +0 -38
  150. package/src/execution/channel-registry-access.ts +0 -144
  151. package/src/execution/concurrency-pool.ts +0 -116
  152. package/src/execution/config.ts +0 -96
  153. package/src/execution/dialog-queue.ts +0 -330
  154. package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
  155. package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
  156. package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
  157. package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
  158. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
  159. package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
  160. package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
  161. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  162. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  163. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
  164. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
  165. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
  166. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
  167. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
  168. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
  169. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
  170. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
  171. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
  172. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
  173. package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
  174. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
  175. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  176. package/src/execution/engine/__tests__/registry.test.ts +0 -120
  177. package/src/execution/engine/__tests__/routing.test.ts +0 -231
  178. package/src/execution/engine/common/data-dir.ts +0 -62
  179. package/src/execution/engine/common/errors.ts +0 -183
  180. package/src/execution/engine/common/event-journal.ts +0 -254
  181. package/src/execution/engine/common/journal-replay.ts +0 -62
  182. package/src/execution/engine/common/kill-chain.ts +0 -221
  183. package/src/execution/engine/common/nesting-guard.ts +0 -50
  184. package/src/execution/engine/common/persona-router.ts +0 -108
  185. package/src/execution/engine/common/pool-manager.ts +0 -226
  186. package/src/execution/engine/common/schema-emulation.ts +0 -189
  187. package/src/execution/engine/common/session-view-projection.ts +0 -51
  188. package/src/execution/engine/engine-discovery.ts +0 -65
  189. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
  190. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  191. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  192. package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
  193. package/src/execution/engine/engines/pi/reader.ts +0 -48
  194. package/src/execution/engine/engines/pi/registration.ts +0 -35
  195. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  196. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  197. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  198. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
  199. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
  200. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  201. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
  202. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
  203. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -567
  204. package/src/execution/engine/engines/zcode/constants.ts +0 -43
  205. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
  206. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  207. package/src/execution/engine/engines/zcode/parser.ts +0 -436
  208. package/src/execution/engine/engines/zcode/preparer.ts +0 -363
  209. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  210. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  211. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -648
  212. package/src/execution/engine/host-task-spec.ts +0 -47
  213. package/src/execution/engine/model-prompt.ts +0 -59
  214. package/src/execution/engine/paths.ts +0 -42
  215. package/src/execution/engine/port.ts +0 -153
  216. package/src/execution/engine/registry.ts +0 -123
  217. package/src/execution/engine/routing.ts +0 -218
  218. package/src/execution/engine/types.ts +0 -304
  219. package/src/execution/execute-options-mapper.ts +0 -111
  220. package/src/execution/execution-record.ts +0 -906
  221. package/src/execution/finalize-record.ts +0 -251
  222. package/src/execution/finalized-marker.ts +0 -51
  223. package/src/execution/get-state-handshake.ts +0 -104
  224. package/src/execution/host-mode.ts +0 -56
  225. package/src/execution/idle-gc.ts +0 -47
  226. package/src/execution/lifecycle-manager.ts +0 -491
  227. package/src/execution/lifecycle-predicates.ts +0 -65
  228. package/src/execution/manifest-store.ts +0 -256
  229. package/src/execution/model-config-service.ts +0 -204
  230. package/src/execution/model-resolver.ts +0 -276
  231. package/src/execution/notifier.ts +0 -260
  232. package/src/execution/output-collector.ts +0 -88
  233. package/src/execution/path-encoding.ts +0 -60
  234. package/src/execution/pi-invocation.ts +0 -120
  235. package/src/execution/record-entry.ts +0 -132
  236. package/src/execution/record-store.ts +0 -1234
  237. package/src/execution/relay-env.ts +0 -37
  238. package/src/execution/session-context-resolver.ts +0 -64
  239. package/src/execution/session-file-gc.ts +0 -120
  240. package/src/execution/session-pending.ts +0 -164
  241. package/src/execution/session-reconstructor.ts +0 -678
  242. package/src/execution/session-runner.ts +0 -1548
  243. package/src/execution/sessions-index.ts +0 -304
  244. package/src/execution/spawn-event-adapter.ts +0 -363
  245. package/src/execution/stdin-writer.ts +0 -198
  246. package/src/execution/stream-sink.ts +0 -126
  247. package/src/execution/subagent-service.ts +0 -2119
  248. package/src/execution/subprocess-agent-runner.ts +0 -303
  249. package/src/execution/temp-prompt.ts +0 -62
  250. package/src/execution/tombstone-store.ts +0 -72
  251. package/src/execution/turn-limiter.ts +0 -102
  252. package/src/execution/types.ts +0 -910
  253. package/src/execution/ui-channels.ts +0 -216
  254. package/src/execution/ui-interaction-model.ts +0 -48
  255. package/src/execution/ui-request-handler-factory.ts +0 -212
  256. package/src/execution/ui-request-observability.ts +0 -81
  257. package/src/execution/ui-request-queue.ts +0 -174
  258. package/src/execution/worktree-manager.ts +0 -688
  259. package/src/execution/worktree-registry.ts +0 -222
  260. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -336
  261. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
  262. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
  263. package/src/orchestration/__tests__/args-validator.test.ts +0 -143
  264. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  265. package/src/orchestration/__tests__/config-loader.test.ts +0 -503
  266. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
  267. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
  268. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
  269. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
  270. package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -314
  271. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
  272. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
  273. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
  274. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
  275. package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
  276. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
  277. package/src/orchestration/__tests__/script-lint.test.ts +0 -831
  278. package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
  279. package/src/orchestration/__tests__/test-mocks.ts +0 -197
  280. package/src/orchestration/__tests__/worker-host.test.ts +0 -120
  281. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
  282. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -520
  283. package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
  284. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
  285. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
  286. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
  287. package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
  288. package/src/orchestration/agent-opts-resolver.ts +0 -93
  289. package/src/orchestration/args-validator.ts +0 -127
  290. package/src/orchestration/concurrency-gate.ts +0 -69
  291. package/src/orchestration/config-loader.ts +0 -315
  292. package/src/orchestration/error-recovery.ts +0 -862
  293. package/src/orchestration/execute-agent-call.ts +0 -217
  294. package/src/orchestration/launcher.ts +0 -410
  295. package/src/orchestration/lifecycle.ts +0 -396
  296. package/src/orchestration/models/__tests__/budget.test.ts +0 -367
  297. package/src/orchestration/models/__tests__/trace.test.ts +0 -408
  298. package/src/orchestration/models/agent-call.ts +0 -83
  299. package/src/orchestration/models/budget.ts +0 -148
  300. package/src/orchestration/models/ports.ts +0 -164
  301. package/src/orchestration/models/run-runtime.ts +0 -89
  302. package/src/orchestration/models/run-spec.ts +0 -83
  303. package/src/orchestration/models/run-state.ts +0 -44
  304. package/src/orchestration/models/trace.ts +0 -183
  305. package/src/orchestration/models/types.ts +0 -292
  306. package/src/orchestration/models/workflow-run.ts +0 -254
  307. package/src/orchestration/models/workflow-script-registry.ts +0 -35
  308. package/src/orchestration/models/workflow-script.ts +0 -117
  309. package/src/orchestration/script-lint.ts +0 -766
  310. package/src/orchestration/skill-discovery.ts +0 -92
  311. package/src/orchestration/worker-handle.ts +0 -115
  312. package/src/orchestration/worker-host.ts +0 -98
  313. package/src/orchestration/worker-script-builder.ts +0 -408
  314. package/src/orchestration/workflow-files.ts +0 -85
  315. package/src/orchestration/workflow-script-registry-impl.ts +0 -133
  316. package/src/shared/__tests__/agent-ref.test.ts +0 -34
  317. package/src/shared/__tests__/meta-parser.test.ts +0 -304
  318. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
  319. package/src/shared/__tests__/resource-discovery.test.ts +0 -736
  320. package/src/shared/__tests__/resource-meta.test.ts +0 -51
  321. package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
  322. package/src/shared/agent-event.ts +0 -13
  323. package/src/shared/agent-ref.ts +0 -57
  324. package/src/shared/meta-parser.ts +0 -261
  325. package/src/shared/resource-discovery.ts +0 -811
  326. package/src/shared/resource-meta.ts +0 -65
  327. package/src/shared/schema-jsonify.ts +0 -56
  328. package/src/shared/xml-injection.ts +0 -35
  329. package/workflows/README.md +0 -81
  330. package/workflows/_shared/agent-refs.cjs +0 -40
  331. package/workflows/chain.js +0 -131
  332. package/workflows/map-reduce.js +0 -174
  333. package/workflows/parallel.js +0 -148
  334. package/workflows/review-fix-loop-utils.cjs +0 -1307
  335. package/workflows/review-fix-loop.js +0 -1396
  336. package/workflows/scatter-gather.js +0 -178
@@ -1,1057 +0,0 @@
1
- // src/__tests__/record-store.test.ts
2
- //
3
- // RecordStore 专属测试。
4
- // 覆盖:
5
- // - archive 立即移除(终态 record 不留内存,读时从 session.jsonl 重建)
6
- // - collectRecords 合并内存(running) + 磁盘(session.jsonl 重建)
7
- // - collectRecords statusFilter("running" vs "all")
8
- // - cancelled tombstone override
9
- // - compareRecords 排序(status priority + startedAt desc)
10
- // - 重建缓存(notifyChange 失效)
11
- //
12
- // 用 tmpdir + 真实 .jsonl fixture(隔离真实文件系统,同 session-reconstructor.test.ts 模式)。
13
-
14
- import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
-
18
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
-
20
- import { writeAliveMarker } from "../alive-store.ts";
21
- import { completeRecord, createRecord, tryTransition } from "../execution-record.ts";
22
- import { writeFinalized } from "../finalized-marker.ts";
23
- import type { ManifestRecord } from "../manifest-store.ts";
24
- import { ManifestStore } from "../manifest-store.ts";
25
- import { getSubagentRecordsDir, getSubagentSessionDir } from "../path-encoding.ts";
26
- import { SUBAGENT_RECORD_CUSTOM_TYPE } from "../record-entry.ts";
27
- import type { StatusFilter } from "../record-store.ts";
28
- import { RecordStore } from "../record-store.ts";
29
- import { writeCancelledTombstone } from "../tombstone-store.ts";
30
- import type { AliveMarker, ExecutionRecord } from "../types.ts";
31
-
32
- /** 构造 ExecutionRecord(base 默认 running,over 覆盖任意字段)。 */
33
- function makeRecord(over: Partial<ExecutionRecord> = {}): ExecutionRecord {
34
- const base = createRecord("r1", {
35
- agent: "worker",
36
- model: "m",
37
- mode: "sync",
38
- task: "t",
39
- startedAt: 1000,
40
- rootSessionId: "sess-current",
41
- // 对齐生产 register 路径(subagent-service createRecord:one-shot 显式 false)
42
- chatMode: false,
43
- });
44
- return { ...base, ...over };
45
- }
46
-
47
- /**
48
- * 写一个最小合法的 session.jsonl(含 identity custom entry + 1 个 assistant message)。
49
- * 用于 collectRecords 磁盘源重建测试。
50
- */
51
- function writeSessionJsonl(
52
- filePath: string,
53
- identity: { id: string; agent: string; mode: "sync" | "background"; task: string; startedAt: number; rootSessionId?: string; parentRecordId?: string; depth?: number; lastTs?: number; chatMode?: boolean },
54
- assistantText = "result text",
55
- ): void {
56
- const lastTs = identity.lastTs ?? identity.startedAt + 1000;
57
- const header = JSON.stringify({
58
- type: "session", version: 3, id: "sess-uuid", timestamp: new Date(identity.startedAt).toISOString(), cwd: "/tmp",
59
- });
60
- const identityData: Record<string, unknown> = {
61
- id: identity.id,
62
- agent: identity.agent,
63
- mode: identity.mode,
64
- task: identity.task,
65
- startedAt: identity.startedAt,
66
- };
67
- if (identity.rootSessionId !== undefined) identityData.rootSessionId = identity.rootSessionId;
68
- if (identity.parentRecordId !== undefined) identityData.parentRecordId = identity.parentRecordId;
69
- if (identity.depth !== undefined) identityData.depth = identity.depth;
70
- if (identity.chatMode !== undefined) identityData.chatMode = identity.chatMode;
71
- const identityEntry = JSON.stringify({
72
- type: "custom",
73
- id: "id-1",
74
- parentId: null,
75
- timestamp: new Date(identity.startedAt).toISOString(),
76
- customType: "subagent-identity",
77
- data: identityData,
78
- });
79
- const assistantMsg = JSON.stringify({
80
- type: "message",
81
- id: "msg-1",
82
- parentId: "id-1",
83
- timestamp: new Date(lastTs).toISOString(),
84
- message: {
85
- role: "assistant",
86
- content: [{ type: "text", text: assistantText }],
87
- usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
88
- stopReason: "stop",
89
- timestamp: lastTs,
90
- },
91
- });
92
- fs.writeFileSync(filePath, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
93
- }
94
-
95
- describe("RecordStore", () => {
96
- let rootDir: string;
97
- let tmpDir: string;
98
-
99
- beforeEach(() => {
100
- // [DS3] 两层布局:tmpDir 深一层(对齐生产 <enc> 段结构),sessions-index.json 落
101
- // dirname(tmpDir)=rootDir 内随 afterEach 清理(含 MF-3/S-20 的嵌套 <enc> 段索引)。
102
- rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "rs-test-"));
103
- tmpDir = path.join(rootDir, "sessions");
104
- fs.mkdirSync(tmpDir);
105
- });
106
- afterEach(() => {
107
- // maxRetries:fire-and-forget 的 sessions-index 写可能与删除并发(ENOTEMPTY 竞态)
108
- fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
109
- });
110
-
111
- // ============================================================
112
- // archive 立即移除
113
- // ============================================================
114
- describe("archive 立即移除", () => {
115
- it("archive 后 record 立即从内存移除(不再 linger)", () => {
116
- const store = new RecordStore(tmpDir);
117
- const r = makeRecord({ id: "sync-1", mode: "sync", status: "closed" });
118
- store.register(r);
119
- expect(store.getMutable("sync-1")).toBeDefined();
120
- store.archive(r);
121
- expect(store.getMutable("sync-1")).toBeUndefined();
122
- });
123
-
124
- it("background record 同样立即移除(不再 FIFO)", () => {
125
- const store = new RecordStore(tmpDir);
126
- const r = makeRecord({ id: "bg-1", mode: "background", status: "closed" });
127
- store.register(r);
128
- store.archive(r);
129
- expect(store.getMutable("bg-1")).toBeUndefined();
130
- });
131
- });
132
-
133
- // ============================================================
134
- // collectRecords:内存(running) + 磁盘(重建) 合并
135
- // ============================================================
136
- describe("collectRecords 合并", () => {
137
- it("内存 running record 出现在结果中", () => {
138
- const store = new RecordStore(tmpDir);
139
- store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
140
- const ids = store.collectRecords(100).map((r) => r.id);
141
- expect(ids).toContain("run-1");
142
- });
143
-
144
- it("磁盘 session.jsonl 重建的 record 出现在结果中(无 sidecar → running, SP-2 跨重启可恢复)", () => {
145
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
146
- writeSessionJsonl(sessionFile, {
147
- id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
148
- });
149
- // 无 sidecar → 四分支兜底 running(v4 B-1:旧 idle 折入 running,SP-2 跨重启可恢复)
150
- const store = new RecordStore(tmpDir);
151
- const found = store.collectRecords(100).find((r) => r.id === "bg-1");
152
- expect(found).toBeDefined();
153
- expect(found?.status).toBe("running");
154
- expect(found?.agent).toBe("worker");
155
- // [perf] 列表是 light(头部 identity,详情字段缺省)——turns/tokens/result 走 getFullRecord 懒加载
156
- expect(found?.turns).toBe(0);
157
- expect(found?.result).toBeUndefined();
158
- const full = store.getFullRecord("bg-1");
159
- expect(full?.turns).toBe(1);
160
- expect(full?.totalTokens).toBe(30);
161
- expect(full?.result).toBe("result text");
162
- expect(full?.status).toBe("running"); // 状态矩阵在全量路径同样套用
163
- });
164
-
165
- it("磁盘 session.jsonl + .finalized sidecar → done", () => {
166
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-b.jsonl");
167
- writeSessionJsonl(sessionFile, {
168
- id: "bg-2", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
169
- });
170
- writeFinalized(sessionFile);
171
- const store = new RecordStore(tmpDir);
172
- const found = store.collectRecords(100).find((r) => r.id === "bg-2");
173
- expect(found).toBeDefined();
174
- expect(found?.status).toBe("closed");
175
- });
176
-
177
- it("statusFilter='running' 含内存 + 磁盘跨重启 running(v4 B-1:旧 idle 折入 running)", () => {
178
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
179
- writeSessionJsonl(sessionFile, {
180
- id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
181
- });
182
- const store = new RecordStore(tmpDir);
183
- store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
184
- const filter: StatusFilter = "running";
185
- const ids = store.collectRecords(100, filter).map((r) => r.id);
186
- // 内存 run-1 + 磁盘跨重启 bg-1 都是 running(旧 idle 折入);startedAt desc → bg-1(5000) 先
187
- expect(ids).toEqual(["bg-1", "run-1"]);
188
- });
189
-
190
- it("statusFilter='all'(默认)返回内存 + 磁盘", () => {
191
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
192
- writeSessionJsonl(sessionFile, {
193
- id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
194
- });
195
- const store = new RecordStore(tmpDir);
196
- store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
197
- const ids = store.collectRecords(100).map((r) => r.id);
198
- expect(ids).toContain("run-1");
199
- expect(ids).toContain("bg-1");
200
- });
201
-
202
- it("内存 running 优先于磁盘同 id(内存覆盖)", () => {
203
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
204
- writeSessionJsonl(sessionFile, {
205
- id: "dup-1", agent: "worker", mode: "background", task: "from disk", startedAt: 5000,
206
- });
207
- const store = new RecordStore(tmpDir);
208
- store.register(makeRecord({ id: "dup-1", mode: "background", status: "running", startedAt: 5000 }));
209
- const found = store.collectRecords(100).find((r) => r.id === "dup-1");
210
- expect(found?.status).toBe("running"); // 内存 running 覆盖磁盘 idle(SP-2)
211
- });
212
-
213
- it("[MF-3/S-20] 跨进程组合:子进程(worktree)写入 enc(ROOT) 的深层 record 被 ROOT store 重建,同 rootSessionId 全树可见、他树被排除", () => {
214
- // MF-3 修复后:worktree 子进程把其子 record 的 session 文件写到统一 enc(ROOT cwd) 段,
215
- // ROOT 进程的 store 扫同一目录重建(旧实现子进程写到 enc(checkout) 段,此处为空)。
216
- const rootCwd = "/Users/x/root-proj";
217
- const sessionsDir = getSubagentSessionDir(tmpDir, rootCwd);
218
- const recordsDir = getSubagentRecordsDir(tmpDir, rootCwd);
219
-
220
- // 父 record A(ROOT 自己 spawn,parentRecordId 缺省=顶层)+ 孙 record C(B 子进程写入,
221
- // 同 rootSessionId、parentRecordId=A、depth=2)+ 他树 record X(rootSessionId 不同)
222
- fs.mkdirSync(sessionsDir, { recursive: true });
223
- writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-a.jsonl"), {
224
- id: "sa-A", agent: "worker", mode: "background", task: "level1", startedAt: 1000, rootSessionId: "root-main",
225
- });
226
- writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-c.jsonl"), {
227
- id: "sa-C", agent: "worker", mode: "background", task: "level3", startedAt: 3000,
228
- rootSessionId: "root-main", parentRecordId: "sa-A", depth: 2,
229
- });
230
- writeSessionJsonl(path.join(sessionsDir, "2026-01-01-uuid-x.jsonl"), {
231
- id: "sa-X", agent: "worker", mode: "background", task: "other root", startedAt: 4000, rootSessionId: "root-other",
232
- });
233
-
234
- // ROOT 进程的 store:sessionsDir/recordsDir 与子进程写盘目录同段(getSubagentSessionDir 同源)
235
- const store = new RecordStore(sessionsDir, new ManifestStore(recordsDir), undefined);
236
- const recs = store.collectRecords(10, "all", "root-main");
237
- const ids = recs.map((r) => r.id);
238
-
239
- // 全树可见:A(顶层)与 C(深度 2,跨进程写入)都在列表,身份字段正确
240
- expect(ids).toContain("sa-A");
241
- expect(ids).toContain("sa-C");
242
- const c = recs.find((r) => r.id === "sa-C");
243
- expect(c?.parentRecordId).toBe("sa-A");
244
- expect(c?.depth).toBe(2);
245
- // 他树 record 被 rootSessionFilter 排除(隔离不破坏)
246
- expect(ids).not.toContain("sa-X");
247
- });
248
- });
249
-
250
- // ============================================================
251
- // cancelled tombstone override
252
- // ============================================================
253
- describe("cancelled tombstone", () => {
254
- it("有 .cancelled sidecar → status override 为 closed + closedReason=cancelled", () => {
255
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
256
- writeSessionJsonl(sessionFile, {
257
- id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
258
- });
259
- writeCancelledTombstone(sessionFile, {
260
- id: "bg-1", status: "cancelled", agent: "worker", startedAt: 5000, endedAt: 6000,
261
- });
262
- const store = new RecordStore(tmpDir);
263
- const found = store.collectRecords(100).find((r) => r.id === "bg-1");
264
- // v4 B-1:cancelled 折入 closed,closedReason='cancelled' 保留用户取消语义
265
- expect(found?.status).toBe("closed");
266
- expect(found?.closedReason).toBe("cancelled");
267
- expect(found?.error).toBe("cancelled by user");
268
- });
269
- });
270
-
271
- // ============================================================
272
- // compareRecords 排序稳定性(内存 running record)
273
- // ============================================================
274
- describe("compareRecords 排序", () => {
275
- it("status priority(running < closed)", () => {
276
- const store = new RecordStore(tmpDir);
277
- // 内存 running record
278
- const running = makeRecord({ id: "run-1", mode: "background", startedAt: 3000, status: "running" });
279
- store.register(running);
280
- // 磁盘 closed record(.finalized sidecar → closed,v4 B-1 统一终态)
281
- const doneFile = path.join(tmpDir, "a.jsonl");
282
- writeSessionJsonl(doneFile, {
283
- id: "done-1", agent: "w", mode: "background", task: "t", startedAt: 5000,
284
- });
285
- writeFinalized(doneFile);
286
- const ids = store.collectRecords(100).map((r) => r.id);
287
- expect(ids[0]).toBe("run-1"); // running(0) 排在 closed(3) 前
288
- });
289
-
290
- it("同 status 时 startedAt desc(新→旧)", () => {
291
- const store = new RecordStore(tmpDir);
292
- writeSessionJsonl(path.join(tmpDir, "old.jsonl"), {
293
- id: "old", agent: "w", mode: "background", task: "t", startedAt: 1000,
294
- });
295
- writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
296
- id: "new", agent: "w", mode: "background", task: "t", startedAt: 9000,
297
- });
298
- // 两个都是 running(磁盘重建,无 sidecar → SP-2 兜底 running,旧 idle 折入),按 startedAt desc
299
- const ids = store.collectRecords(100).map((r) => r.id);
300
- expect(ids).toEqual(["new", "old"]);
301
- });
302
- });
303
-
304
- // ============================================================
305
- // running 态 record(M2-A chatMode 续聊;v4 B-1:旧 idle 折入 running)
306
- // ============================================================
307
- describe("running 态 record(旧 idle 折入,等待续聊)", () => {
308
- it("getMutable 能查到 running record(不筛 status,running record 留内存不 archive)", () => {
309
- const store = new RecordStore(tmpDir);
310
- const r = makeRecord({ id: "idle-1", status: "running" });
311
- store.register(r);
312
- // running record 不 archive → getMutable 仍可查(续聊定位依赖此)
313
- expect(store.getMutable("idle-1")).toBe(r);
314
- });
315
-
316
- it("running record 经 collectRecords(statusFilter=all) 可见", () => {
317
- const store = new RecordStore(tmpDir);
318
- store.register(makeRecord({ id: "idle-2", status: "running" }));
319
- const ids = store.collectRecords(100, "all").map((r) => r.id);
320
- expect(ids).toContain("idle-2");
321
- });
322
-
323
- it("running record 经 collectRecords(statusFilter=running) 可见(v4 B-1:旧 idle 折入 running)", () => {
324
- const store = new RecordStore(tmpDir);
325
- store.register(makeRecord({ id: "idle-3", status: "running" }));
326
- const ids = store.collectRecords(100, "running").map((r) => r.id);
327
- expect(ids).toContain("idle-3");
328
- });
329
- });
330
-
331
-
332
- // ============================================================
333
- // 重建缓存
334
- // ============================================================
335
- describe("重建缓存", () => {
336
- it("新 session.jsonl 无需内存事件即立即可见([perf] stat 校验取代 notifyChange 整体失效)", () => {
337
- const store = new RecordStore(tmpDir);
338
- // 首次 collect:空目录
339
- expect(store.collectRecords(100)).toHaveLength(0);
340
- // 写新 session.jsonl(模拟外部进程/子进程写入——不经本进程任何内存事件)
341
- writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
342
- id: "bg-1", agent: "w", mode: "background", task: "t", startedAt: 1000,
343
- });
344
- // [perf] 旧实现:reconCache 建立后只能靠 register/archive 触发 notifyChange 失效,
345
- // 否则永远 stale(外部进程写入的文件对本进程不可见)。新实现:每次 collectRecords
346
- // 都 readdir + stat 校验,新文件立即可见(同时修复跨进程可见性)。
347
- const ids = store.collectRecords(100).map((r) => r.id);
348
- expect(ids).toContain("bg-1");
349
- });
350
- });
351
-
352
- // ============================================================
353
- // dispose / revive
354
- // ============================================================
355
- describe("dispose / revive", () => {
356
- it("dispose 后 notifyChange 不再触发 listener", () => {
357
- const store = new RecordStore(tmpDir);
358
- let count = 0;
359
- store.onChange(() => { count++; });
360
- store.register(makeRecord({ id: "r1", startedAt: 1000 }));
361
- expect(count).toBe(1);
362
- store.dispose();
363
- store.register(makeRecord({ id: "r2", startedAt: 2000 }));
364
- expect(count).toBe(1); // dispose 后不再通知
365
- });
366
- });
367
-
368
- // ============================================================
369
- // 四分支 sidecar 矩阵(D-006 + D-021)
370
- // ============================================================
371
- describe("四分支 sidecar 矩阵", () => {
372
- const SESSION_ID = "bg-1";
373
- const STARTED_AT = 1000;
374
-
375
- function writeBaseSession(): string {
376
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-sidecar.jsonl");
377
- writeSessionJsonl(sessionFile, {
378
- id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: STARTED_AT,
379
- });
380
- return sessionFile;
381
- }
382
-
383
- // ── 分支 1: .cancelled ──
384
- it(".cancelled sidecar → closed + closedReason=cancelled", () => {
385
- const sessionFile = writeBaseSession();
386
- writeCancelledTombstone(sessionFile, {
387
- id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
388
- });
389
- const store = new RecordStore(tmpDir);
390
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
391
- // v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消)
392
- expect(found?.status).toBe("closed");
393
- expect(found?.closedReason).toBe("cancelled");
394
- expect(found?.error).toBe("cancelled by user");
395
- expect(found?.endedAt).toBe(6000);
396
- });
397
-
398
- // ── 分支 2: .finalized done ──
399
- it(".finalized sidecar + stopReason=stop → done", () => {
400
- const sessionFile = writeBaseSession();
401
- writeFinalized(sessionFile);
402
- const store = new RecordStore(tmpDir);
403
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
404
- expect(found?.status).toBe("closed");
405
- });
406
-
407
- // ── 分支 2: .finalized failed ──
408
- it(".finalized sidecar + stopReason=error → failed", () => {
409
- // 写一个 stopReason=error 的 session.jsonl
410
- const sessionFile = path.join(tmpDir, "2026-01-01-uuid-fail.jsonl");
411
- const header = JSON.stringify({
412
- type: "session", version: 3, id: "sess-uuid", timestamp: new Date(STARTED_AT).toISOString(), cwd: "/tmp",
413
- });
414
- const identityEntry = JSON.stringify({
415
- type: "custom", id: "id-1", parentId: null, timestamp: new Date(STARTED_AT).toISOString(),
416
- customType: "subagent-identity",
417
- data: { id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: STARTED_AT },
418
- });
419
- const assistantMsg = JSON.stringify({
420
- type: "message", id: "msg-1", parentId: "id-1",
421
- timestamp: new Date(STARTED_AT + 1000).toISOString(),
422
- message: {
423
- role: "assistant",
424
- content: [{ type: "text", text: "error output" }],
425
- usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
426
- stopReason: "error",
427
- errorMessage: "something went wrong",
428
- timestamp: STARTED_AT + 1000,
429
- },
430
- });
431
- fs.writeFileSync(sessionFile, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
432
-
433
- writeFinalized(sessionFile);
434
- const store = new RecordStore(tmpDir);
435
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
436
- expect(found?.status).toBe("closed");
437
- });
438
-
439
- // ── 分支 3: .alive + 活 pid → running + externalInstance ──
440
- it(".alive + 存活 pid → running + externalInstance=true", () => {
441
- const sessionFile = writeBaseSession();
442
- const recentStartedAt = Date.now() - 1000; // 1 秒前,确保未超 24h
443
- const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: recentStartedAt };
444
- writeAliveMarker(sessionFile, marker);
445
- const store = new RecordStore(tmpDir);
446
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
447
- expect(found?.status).toBe("running");
448
- expect(found?.externalInstance).toEqual(marker);
449
- });
450
-
451
- // ── 分支 3→4: .alive + 死 pid → running(SP-2:跨重启可恢复)──
452
- it(".alive + 死 pid → running(SP-2 跨重启可恢复)", () => {
453
- const sessionFile = writeBaseSession();
454
- const marker: AliveMarker = { pid: 9999999, id: SESSION_ID, startedAt: STARTED_AT };
455
- writeAliveMarker(sessionFile, marker);
456
- const store = new RecordStore(tmpDir);
457
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
458
- expect(found?.status).toBe("running");
459
- expect(found?.externalInstance).toBeUndefined();
460
- });
461
-
462
- // ── 分支 4: 都无 sidecar → running(SP-2:跨重启可恢复)──
463
- it("无任何 sidecar → running(SP-2 跨重启可恢复)", () => {
464
- writeBaseSession();
465
- const store = new RecordStore(tmpDir);
466
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
467
- expect(found?.status).toBe("running");
468
- });
469
-
470
- // ── 分支 4: >24h 软超时 → running(SP-2:跨重启可恢复,无视探活)──
471
- it(">24h 软超时 → running(SP-2,即使 pid 存活)", () => {
472
- const sessionFile = writeBaseSession();
473
- // startedAt 设为 25 小时前
474
- const oldStartedAt = Date.now() - 25 * 60 * 60 * 1000;
475
- const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: oldStartedAt };
476
- writeAliveMarker(sessionFile, marker);
477
-
478
- // 重写 session.jsonl 使 startedAt 匹配
479
- fs.unlinkSync(sessionFile);
480
- writeSessionJsonl(sessionFile, {
481
- id: SESSION_ID, agent: "worker", mode: "background", task: "do it", startedAt: oldStartedAt,
482
- });
483
-
484
- const store = new RecordStore(tmpDir);
485
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
486
- expect(found?.status).toBe("running");
487
- expect(found?.externalInstance).toBeUndefined();
488
- });
489
-
490
- // ── 回归:.cancelled 优先于 .finalized ──
491
- it(".cancelled 优先于 .finalized(即使两者共存)", () => {
492
- const sessionFile = writeBaseSession();
493
- writeFinalized(sessionFile);
494
- writeCancelledTombstone(sessionFile, {
495
- id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
496
- });
497
- const store = new RecordStore(tmpDir);
498
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
499
- // v4 B-1:.cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
500
- expect(found?.status).toBe("closed");
501
- expect(found?.closedReason).toBe("cancelled");
502
- });
503
-
504
- // ── 回归:旧 .cancelled 单分支行为不变 ──
505
- it("旧 .cancelled 单分支行为不变(回归,status 折入 closed)", () => {
506
- const sessionFile = writeBaseSession();
507
- writeCancelledTombstone(sessionFile, {
508
- id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 7000,
509
- });
510
- const store = new RecordStore(tmpDir);
511
- const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
512
- expect(found?.status).toBe("closed");
513
- expect(found?.closedReason).toBe("cancelled");
514
- expect(found?.error).toBe("cancelled by user");
515
- expect(found?.endedAt).toBe(7000);
516
- });
517
- });
518
-
519
- // ============================================================
520
- // session 隔离(问题 1 修复)
521
- // ============================================================
522
- describe("session 隔离(rootSessionId 过滤)", () => {
523
- it("磁盘源:只返回 rootSessionId 匹配的 record", () => {
524
- writeSessionJsonl(path.join(tmpDir, "mine.jsonl"), {
525
- id: "mine", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
526
- });
527
- writeSessionJsonl(path.join(tmpDir, "other.jsonl"), {
528
- id: "other", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
529
- });
530
- const store = new RecordStore(tmpDir);
531
- const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
532
- expect(ids).toEqual(["mine"]); // other 属于 sess-B,被隔离
533
- });
534
-
535
- it("磁盘源:rootSessionId 缺失(旧文件)被排除(无法判定归属)", () => {
536
- writeSessionJsonl(path.join(tmpDir, "legacy.jsonl"), {
537
- id: "legacy", agent: "w", mode: "background", task: "t", startedAt: 1000, // 无 rootSessionId
538
- });
539
- const store = new RecordStore(tmpDir);
540
- const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
541
- expect(ids).toEqual([]); // 旧文件被排除
542
- });
543
-
544
- it("磁盘源:不传 filter(undefined)不过滤(向后兼容)", () => {
545
- writeSessionJsonl(path.join(tmpDir, "legacy.jsonl"), {
546
- id: "legacy", agent: "w", mode: "background", task: "t", startedAt: 1000,
547
- });
548
- writeSessionJsonl(path.join(tmpDir, "tagged.jsonl"), {
549
- id: "tagged", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-A",
550
- });
551
- const store = new RecordStore(tmpDir);
552
- const ids = store.collectRecords(100).map((r) => r.id);
553
- expect(ids.sort()).toEqual(["legacy", "tagged"]); // 全返回,不过滤
554
- });
555
-
556
- it("内存源:只返回 rootSessionId 匹配的 running record", () => {
557
- const store = new RecordStore(tmpDir);
558
- store.register(makeRecord({ id: "mine", startedAt: 1000, rootSessionId: "sess-A" }));
559
- store.register(makeRecord({ id: "other", startedAt: 2000, rootSessionId: "sess-B" }));
560
- const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id);
561
- expect(ids).toEqual(["mine"]);
562
- });
563
-
564
- it("内存+磁盘混合:同时按 session 过滤", () => {
565
- writeSessionJsonl(path.join(tmpDir, "disk-A.jsonl"), {
566
- id: "disk-A", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
567
- });
568
- writeSessionJsonl(path.join(tmpDir, "disk-B.jsonl"), {
569
- id: "disk-B", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
570
- });
571
- const store = new RecordStore(tmpDir);
572
- store.register(makeRecord({ id: "mem-A", startedAt: 3000, rootSessionId: "sess-A" }));
573
- store.register(makeRecord({ id: "mem-B", startedAt: 4000, rootSessionId: "sess-B" }));
574
- const ids = store.collectRecords(100, "all", "sess-A").map((r) => r.id).sort();
575
- expect(ids).toEqual(["disk-A", "mem-A"]);
576
- });
577
-
578
- it("重建缓存:不同 filter 共享缓存,不交叉污染", () => {
579
- writeSessionJsonl(path.join(tmpDir, "a.jsonl"), {
580
- id: "a", agent: "w", mode: "background", task: "t", startedAt: 1000, rootSessionId: "sess-A",
581
- });
582
- writeSessionJsonl(path.join(tmpDir, "b.jsonl"), {
583
- id: "b", agent: "w", mode: "background", task: "t", startedAt: 2000, rootSessionId: "sess-B",
584
- });
585
- const store = new RecordStore(tmpDir);
586
- // 先查 sess-A(建缓存)
587
- expect(store.collectRecords(100, "all", "sess-A").map((r) => r.id)).toEqual(["a"]);
588
- // 再查 sess-B(复用缓存基底,过滤不交叉)
589
- expect(store.collectRecords(100, "all", "sess-B").map((r) => r.id)).toEqual(["b"]);
590
- // 不带 filter(复用缓存,全量)
591
- expect(store.collectRecords(100).map((r) => r.id).sort()).toEqual(["a", "b"]);
592
- });
593
- });
594
-
595
- // ============================================================
596
- // endedAt 重建(问题 2 修复:终态耗时不再随墙钟增长)
597
- // ============================================================
598
- describe("endedAt 重建(耗时不再无限增长)", () => {
599
- it(".finalized → light endedAt 用 mtime 近似,全量 endedAt 为最后 entry 时间戳", () => {
600
- const sessionFile = path.join(tmpDir, "fin.jsonl");
601
- writeSessionJsonl(sessionFile, {
602
- id: "bg-1", agent: "w", mode: "background", task: "t",
603
- startedAt: 5000, lastTs: 9000, rootSessionId: "sess-A",
604
- });
605
- writeFinalized(sessionFile);
606
- const store = new RecordStore(tmpDir);
607
- const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
608
- // [perf] light 分支 2:endedAt 用 jsonl mtime 近似(finalize 后文件不再变化,
609
- // 与最后 entry ts 差 <1s)——不是 entry ts 9000,也不是随墙钟无限增长的 now 基准。
610
- expect(found?.endedAt).toBeDefined();
611
- expect(found?.endedAt).toBeGreaterThan(5000);
612
- // 全量(getFullRecord):精确用最后 entry 时间戳(非 now)。
613
- expect(store.getFullRecord("bg-1")?.endedAt).toBe(9000);
614
- });
615
-
616
- it("无 sidecar(running, SP-2)→ endedAt 保持 undefined(非终态)", () => {
617
- const sessionFile = path.join(tmpDir, "crash.jsonl");
618
- writeSessionJsonl(sessionFile, {
619
- id: "bg-1", agent: "w", mode: "background", task: "t",
620
- startedAt: 5000, lastTs: 9000, rootSessionId: "sess-A",
621
- });
622
- const store = new RecordStore(tmpDir);
623
- const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
624
- expect(found?.status).toBe("running");
625
- // SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined(待续聊)
626
- expect(found?.endedAt).toBeUndefined();
627
- });
628
-
629
- it(".alive running → endedAt 保持 undefined(running 耗时继续增长是正确的)", () => {
630
- const sessionFile = path.join(tmpDir, "alive.jsonl");
631
- writeSessionJsonl(sessionFile, {
632
- id: "bg-1", agent: "w", mode: "background", task: "t",
633
- startedAt: Date.now() - 1000, lastTs: Date.now(), rootSessionId: "sess-A",
634
- });
635
- writeAliveMarker(sessionFile, { pid: process.pid, id: "bg-1", startedAt: Date.now() - 1000 });
636
- const store = new RecordStore(tmpDir);
637
- const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
638
- expect(found?.status).toBe("running");
639
- expect(found?.endedAt).toBeUndefined();
640
- });
641
- });
642
-
643
- // ============================================================
644
- // [BL-M1] 修复:损坏 manifest 上报 pi.appendEntry(用户可见)
645
- // collectRecords 跳过 mapManifestStatus 返回 null 的 manifest 时,除 console.warn
646
- // 外还需调 pi.appendEntry("subagent:manifest-invalid-status", ...) 让用户能在 session
647
- // 内复盘事故——避免磁盘陈旧/损坏 manifest 被静默吞掉。
648
- // ============================================================
649
- describe("manifest status 越界(损坏 manifest)", () => {
650
- /** 构造一个 status 越界的 manifest mock。
651
- * ManifestRecord.status union 是 4 态,运行时却可能收到陈旧 "error" / 意外 "crashed"
652
- * 等越界值——isValidManifest 当前挡掉了大部分,但 manifestStore.listAllSync 与
653
- * mapManifestStatus 之间仍有防御层([BL-M1] 修复记录)。本测试用 unknown-cast 模拟
654
- * 一个越过 ManifestRecord 静态类型的对象,验证 collectRecords 反应正确。 */
655
- function brokenManifest(): ManifestRecord {
656
- // eslint-disable-next-line taste/no-unsafe-cast -- intentional: cast status 越界值模拟磁盘损坏 manifest(测试目的本身)
657
- return {
658
- id: "broken-1",
659
- rootSessionId: "sess-current",
660
- agentName: "worker",
661
- status: "crashed", // 越界值(union 不含 "crashed",SP-1 后 valid statuses = running/closed/cancelled)
662
- createdAt: 1000,
663
- } as unknown as ManifestRecord;
664
- }
665
-
666
- /** Mock ManifestStore 让测试不依赖 fs 路径 + isValidManifest 守卫。
667
- * source/target 类型不兼容是 intentional(ManifestStore 有多个方法,测试仅需 listAllSync),
668
- * `as unknown as ManifestStore` 是 vitest mock 标准 pattern。*/
669
- function makeManifestStoreMock(manifests: ManifestRecord[]): ManifestStore {
670
- // eslint-disable-next-line taste/no-unsafe-cast -- intentional: ManifestStore 有多个方法,测试仅需 listAllSync
671
- return {
672
- // 复制 manifests 防调用方意外 mutate 共享数组;vi.fn 每次调用返回独立数组
673
- listAllSync: vi.fn(() => [...manifests]),
674
- } as unknown as ManifestStore;
675
- }
676
-
677
- it("collectRecords 跳过越界 status 时调用 pi.appendEntry('subagent:manifest-invalid-status', ...)", () => {
678
- const pi = { appendEntry: vi.fn() };
679
- // Mock ManifestStore:listAllSync 返回损坏 manifest,绕开真实 fs 路径与 isValidManifest 守卫。
680
- const manifestStore = makeManifestStoreMock([brokenManifest()]);
681
- const store = new RecordStore(tmpDir, manifestStore, pi);
682
-
683
- const result = store.collectRecords(100);
684
-
685
- // 损坏 manifest 不出现在结果中(status 越界被跳过,不降级为 failed——避免误告警)
686
- expect(result.find((r) => r.id === "broken-1")).toBeUndefined();
687
- // appendEntry 被调用,customType + data 字段正确
688
- expect(pi.appendEntry).toHaveBeenCalledTimes(1);
689
- expect(pi.appendEntry).toHaveBeenCalledWith("subagent:manifest-invalid-status", {
690
- id: "broken-1",
691
- status: "crashed",
692
- rootSessionId: "sess-current",
693
- agentName: "worker",
694
- });
695
- });
696
-
697
- it("pi 为 null/undefined 时不抛错(向后兼容:不注入 pi 的旧调用路径)", () => {
698
- const manifestStore = makeManifestStoreMock([brokenManifest()]);
699
- // 不传 pi(undefined)—— 模拟 SubagentService 构造期 session_start 未触发的场景。
700
- const store = new RecordStore(tmpDir, manifestStore);
701
- expect(() => store.collectRecords(100)).not.toThrow();
702
- // 损坏 manifest 仍被跳过(不降级)
703
- const result = store.collectRecords(100);
704
- expect(result.find((r) => r.id === "broken-1")).toBeUndefined();
705
- });
706
-
707
- it("setPi() 后 appendEntry 切换到新 pi(覆盖初始 undefined)", () => {
708
- const pi1 = { appendEntry: vi.fn() };
709
- const pi2 = { appendEntry: vi.fn() };
710
- const manifestStore = makeManifestStoreMock([brokenManifest()]);
711
- const store = new RecordStore(tmpDir, manifestStore, pi1);
712
- store.setPi(pi2);
713
-
714
- store.collectRecords(100);
715
-
716
- // 切换后只调 pi2(pi1 不再被调用)
717
- expect(pi1.appendEntry).not.toHaveBeenCalled();
718
- expect(pi2.appendEntry).toHaveBeenCalledTimes(1);
719
- expect(pi2.appendEntry).toHaveBeenCalledWith(
720
- "subagent:manifest-invalid-status",
721
- expect.objectContaining({ id: "broken-1" }),
722
- );
723
- });
724
- });
725
-
726
- // ============================================================
727
- // SP-2: reconstructAll 兜底分支输出 running(非 crashed;v4 两态后旧 idle 折入 running)
728
- // ============================================================
729
- describe("SP-2: reconstructAll 兜底 running(跨重启恢复)", () => {
730
- // TC-1: 兜底分支输出 running(非 crashed)
731
- it("TC-1: 无 sidecar + 死 pid → reconstructAll 输出 running(可冷路径 resume)", () => {
732
- const sessionFile = path.join(tmpDir, "sp2-tc1.jsonl");
733
- writeSessionJsonl(sessionFile, {
734
- id: "sa-sp2-1", agent: "worker", mode: "background", task: "cross-restart task",
735
- startedAt: 1000, rootSessionId: "sess-sp2",
736
- });
737
- // 无任何 sidecar marker → 分支 4 兜底
738
- const store = new RecordStore(tmpDir);
739
- const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-1");
740
- expect(found).toBeDefined();
741
- expect(found?.status).toBe("running");
742
- // SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined
743
- expect(found?.endedAt).toBeUndefined();
744
- });
745
-
746
- // TC-2: 有 .finalized marker 的 record 仍归档为 done(不回归)
747
- it("TC-2: .finalized marker → done(分支 1/2 不回归)", () => {
748
- const sessionFile = path.join(tmpDir, "sp2-tc2.jsonl");
749
- writeSessionJsonl(sessionFile, {
750
- id: "sa-sp2-2", agent: "worker", mode: "background", task: "finalized task",
751
- startedAt: 2000, rootSessionId: "sess-sp2",
752
- });
753
- writeFinalized(sessionFile);
754
- const store = new RecordStore(tmpDir);
755
- const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2");
756
- expect(found).toBeDefined();
757
- // 分支 2 仍归档为 done,不受 SP-2 影响
758
- expect(found?.status).toBe("closed");
759
- expect(found?.endedAt).toBeDefined();
760
- });
761
-
762
- // TC-2b: .cancelled marker → closed+cancelled(分支 1 不回归)
763
- it("TC-2b: .cancelled marker → closed+cancelled reason(分支 1 不回归)", () => {
764
- const sessionFile = path.join(tmpDir, "sp2-tc2b.jsonl");
765
- writeSessionJsonl(sessionFile, {
766
- id: "sa-sp2-2b", agent: "worker", mode: "background", task: "cancelled task",
767
- startedAt: 3000, rootSessionId: "sess-sp2",
768
- });
769
- writeCancelledTombstone(sessionFile, {
770
- id: "sa-sp2-2b", status: "cancelled", agent: "worker", startedAt: 3000, endedAt: 4000,
771
- });
772
- const store = new RecordStore(tmpDir);
773
- const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2b");
774
- expect(found).toBeDefined();
775
- expect(found?.status).toBe("closed");
776
- expect(found?.closedReason).toBe("cancelled");
777
- });
778
- });
779
-
780
- // ============================================================
781
- // 孤儿终态恢复(residual-fixes):分支 4 兜底 record 的判定与落盘
782
- // ============================================================
783
- describe("recoverOrphanRecords 孤儿终态恢复", () => {
784
- /** 带真实磁盘 fixture + appendEntry 捕获的 store。 */
785
- function makeRecoveryStore(): { store: RecordStore; appended: Array<{ customType: string; data: Record<string, unknown> }> } {
786
- const appended: Array<{ customType: string; data: Record<string, unknown> }> = [];
787
- const store = new RecordStore(tmpDir, undefined, {
788
- appendEntry: (customType: string, data: unknown) => {
789
- appended.push({ customType, data: data as Record<string, unknown> });
790
- },
791
- } as never);
792
- return { store, appended };
793
- }
794
-
795
- it("末行完整 → closed 终态 entry + .finalized sidecar(防重锚)", () => {
796
- const sessionFile = path.join(tmpDir, "orphan-done.jsonl");
797
- writeSessionJsonl(sessionFile, {
798
- id: "sa-orphan-1", agent: "worker", mode: "background", task: "orphan done",
799
- startedAt: 1000, rootSessionId: "sess-orphan",
800
- });
801
- const { store, appended } = makeRecoveryStore();
802
- store.recoverOrphanRecords("sess-orphan");
803
-
804
- const entry = appended.find((c) => c.data.id === "sa-orphan-1");
805
- expect(entry?.customType).toBe("subagent-record");
806
- expect(entry?.data.status).toBe("closed");
807
- expect(entry?.data.closedReason).toBe("gc");
808
- expect(entry?.data.endedAt).toEqual(expect.any(Number));
809
- expect(entry?.data.error).toBeUndefined();
810
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
811
- // 防重:sidecar 使下次重建走分支 2(closed),不再进判定
812
- const again = [...appended];
813
- store.recoverOrphanRecords("sess-orphan");
814
- expect(appended.length).toBe(again.length);
815
- const found = store.collectRecords(100, "all", "sess-orphan").find((r) => r.id === "sa-orphan-1");
816
- expect(found?.status).toBe("closed");
817
- });
818
-
819
- it("末行截断 → closed + error(保守方向)", () => {
820
- const sessionFile = path.join(tmpDir, "orphan-truncated.jsonl");
821
- writeSessionJsonl(sessionFile, {
822
- id: "sa-orphan-2", agent: "worker", mode: "background", task: "orphan truncated",
823
- startedAt: 2000, rootSessionId: "sess-orphan",
824
- });
825
- // 制造截断:append 半行 JSON(无换行结尾)
826
- fs.appendFileSync(sessionFile, '{"type":"message","id":"msg-2","pare', "utf-8");
827
- const { store, appended } = makeRecoveryStore();
828
- store.recoverOrphanRecords("sess-orphan");
829
-
830
- const entry = appended.find((c) => c.data.id === "sa-orphan-2");
831
- expect(entry?.data.status).toBe("closed");
832
- expect(entry?.data.error).toContain("truncated");
833
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
834
- });
835
-
836
- it("末行 >64KB 完整 JSON → 不误判截断,判 done(V1 探针实测回归)", () => {
837
- const sessionFile = path.join(tmpDir, "orphan-longline.jsonl");
838
- writeSessionJsonl(sessionFile, {
839
- id: "sa-orphan-5", agent: "worker", mode: "background", task: "orphan long line",
840
- startedAt: 5000, rootSessionId: "sess-orphan",
841
- });
842
- // 真实库实测形态:末行为超长完整 entry(subagent-identity 的 task 内嵌大 payload,
843
- // 28/4822 个文件末行 65KB-776KB)——固定 64KB 尾窗曾把这类行从中间切开误判截断。
844
- const bigEntry = JSON.stringify({
845
- type: "custom", id: "id-big", parentId: null,
846
- timestamp: new Date(5000).toISOString(), customType: "subagent-identity",
847
- data: { id: "sa-orphan-5", agent: "worker", mode: "background", task: "x".repeat(70 * 1024), startedAt: 5000 },
848
- });
849
- fs.appendFileSync(sessionFile, bigEntry + "\n", "utf-8");
850
- const { store, appended } = makeRecoveryStore();
851
- store.recoverOrphanRecords("sess-orphan");
852
-
853
- const entry = appended.find((c) => c.data.id === "sa-orphan-5");
854
- expect(entry?.data.status).toBe("closed");
855
- expect(entry?.data.error).toBeUndefined();
856
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
857
- });
858
-
859
- it("chatMode 孤儿 → 不终态化,落 resumable entry(可续聊产品语义)", () => {
860
- const sessionFile = path.join(tmpDir, "orphan-chat.jsonl");
861
- writeSessionJsonl(sessionFile, {
862
- id: "sa-orphan-3", agent: "worker", mode: "background", task: "orphan chat",
863
- startedAt: 3000, rootSessionId: "sess-orphan", chatMode: true,
864
- });
865
- const { store, appended } = makeRecoveryStore();
866
- store.recoverOrphanRecords("sess-orphan");
867
-
868
- const entry = appended.find((c) => c.data.id === "sa-orphan-3");
869
- expect(entry?.data.status).toBe("running");
870
- expect(entry?.data.resumable).toBe(true);
871
- expect(entry?.data.chatMode).toBe(true);
872
- // chat 分流不写终态 sidecar
873
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
874
- // orphanJudged 缓存:同 store 重复调用不重复 append
875
- const count = appended.length;
876
- store.recoverOrphanRecords("sess-orphan");
877
- expect(appended.length).toBe(count);
878
- });
879
-
880
- it("非分支 4 record(closed / 活进程)不进判定", () => {
881
- const closedFile = path.join(tmpDir, "orphan-closed.jsonl");
882
- writeSessionJsonl(closedFile, {
883
- id: "sa-orphan-4", agent: "worker", mode: "background", task: "already closed",
884
- startedAt: 4000, rootSessionId: "sess-orphan",
885
- });
886
- writeFinalized(closedFile);
887
- const { store, appended } = makeRecoveryStore();
888
- store.recoverOrphanRecords("sess-orphan");
889
- expect(appended.find((c) => c.data.id === "sa-orphan-4")).toBeUndefined();
890
- });
891
-
892
- /** 写主 session fixture(含指定 subagent-record entry 序列)。 */
893
- function writeMainSession(entries: Array<Record<string, unknown>>): string {
894
- const mainFile = path.join(tmpDir, "main-session.jsonl");
895
- const lines = entries.map((d) => JSON.stringify({ type: "custom", id: `e-${Math.random()}`, parentId: null, customType: "subagent-record", data: d }));
896
- fs.writeFileSync(mainFile, lines.join("\n") + "\n", "utf-8");
897
- return mainFile;
898
- }
899
-
900
- it("entry-born 孤儿(无子文件,spawn 窗口期死亡)→ closed+gc+error(E2E 实测回归)", () => {
901
- const mainFile = writeMainSession([
902
- { v: 1, id: "sa-entryonly-1", agent: "worker", task: "spawn interrupted", slug: "s", status: "running", mode: "background", startedAt: 6000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
903
- ]);
904
- const { store, appended } = makeRecoveryStore();
905
- store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
906
-
907
- const entry = appended.find((c) => c.data.id === "sa-entryonly-1");
908
- expect(entry?.data.status).toBe("closed");
909
- expect(entry?.data.closedReason).toBe("gc");
910
- expect(entry?.data.error).toContain("no child session file");
911
- // 防重:orphanJudged 缓存拦截二次判定
912
- const count = appended.length;
913
- store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
914
- expect(appended.length).toBe(count);
915
- });
916
-
917
- it("entry-born chatMode 孤儿 → 不终态化,落 resumable entry", () => {
918
- const mainFile = writeMainSession([
919
- { v: 1, id: "sa-entryonly-2", agent: "worker", task: "chat spawn interrupted", slug: "s", status: "running", mode: "background", startedAt: 7000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [], chatMode: true },
920
- ]);
921
- const { store, appended } = makeRecoveryStore();
922
- store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
923
-
924
- const entry = appended.find((c) => c.data.id === "sa-entryonly-2");
925
- expect(entry?.data.status).toBe("running");
926
- expect(entry?.data.resumable).toBe(true);
927
- expect(entry?.data.error).toBeUndefined();
928
- });
929
-
930
- it("有子文件锚 / 末条已终态 / 他 session 的 entry-born id 不进判定", () => {
931
- const anchoredFile = path.join(tmpDir, "orphan-anchored.jsonl");
932
- writeSessionJsonl(anchoredFile, {
933
- id: "sa-anchored", agent: "worker", mode: "background", task: "has file",
934
- startedAt: 8000, rootSessionId: "sess-orphan",
935
- });
936
- const mainFile = writeMainSession([
937
- { v: 1, id: "sa-anchored", agent: "worker", task: "has file", slug: "s", status: "running", mode: "background", startedAt: 8000, rootSessionId: "sess-orphan", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
938
- { v: 1, id: "sa-settled", agent: "worker", task: "settled", slug: "s", status: "closed", mode: "background", startedAt: 8100, rootSessionId: "sess-orphan", depth: 0, turns: 1, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
939
- { v: 1, id: "sa-foreign", agent: "worker", task: "other session", slug: "s", status: "running", mode: "background", startedAt: 8200, rootSessionId: "sess-other", depth: 0, turns: 0, totalTokens: 0, model: "m", eventLog: [], displayItems: [] },
940
- ]);
941
- const { store, appended } = makeRecoveryStore();
942
- store.recoverEntryOnlyOrphans(mainFile, "sess-orphan");
943
- expect(appended.find((c) => c.data.id === "sa-anchored")).toBeUndefined();
944
- expect(appended.find((c) => c.data.id === "sa-settled")).toBeUndefined();
945
- expect(appended.find((c) => c.data.id === "sa-foreign")).toBeUndefined();
946
- });
947
- });
948
-
949
- // ============================================================
950
- // W16 [D4]:subagent-record 自描述 appendEntry 上报(状态迁移点)
951
- // ============================================================
952
- describe("W16 subagent-record 自描述 appendEntry 上报", () => {
953
- /** appendEntry 捕获(RecordStorePi 最小实现)。 */
954
- interface AppendedCall {
955
- customType: string;
956
- data: unknown;
957
- }
958
-
959
- /** 构造带 appendEntry 捕获的 store(pi 注入通道,对齐生产 setPi 后形态)。 */
960
- function makeStoreWithPi(): { store: RecordStore; appended: AppendedCall[] } {
961
- const appended: AppendedCall[] = [];
962
- const store = new RecordStore(tmpDir, undefined, {
963
- appendEntry: (customType: string, data: unknown) => {
964
- appended.push({ customType, data });
965
- },
966
- });
967
- return { store, appended };
968
- }
969
-
970
- /** unknown → 对象的运行时 guard(taste/no-unsafe-cast:断言前先收窄)。 */
971
- function asEntryData(d: unknown): Record<string, unknown> {
972
- if (typeof d !== "object" || d === null) throw new Error("entry data is not an object");
973
- return d as Record<string, unknown>;
974
- }
975
-
976
- it("register:append subagent-record entry,data = v1 + SubagentRecord 完整快照 schema", () => {
977
- const { store, appended } = makeStoreWithPi();
978
- store.register(makeRecord());
979
-
980
- expect(appended).toHaveLength(1);
981
- // 写点字面量与 record-entry.ts 常量等值(钉住双源一致性)
982
- expect(appended[0]?.customType).toBe(SUBAGENT_RECORD_CUSTOM_TYPE);
983
- // [E2E 实测教训] 必须先 JSON 序列化再断言:appendEntry 捕获的是内存对象,
984
- // undefined 值的键名仍在(旧 27-key 断言因此漏检 recordToSubagent 丢
985
- // chatMode 的 bug);真实 JSONL 会丢 undefined 值键,此处对齐生产行为。
986
- const data = asEntryData(JSON.parse(JSON.stringify(appended[0]?.data)));
987
- // register 时点序列化存活字段(undefined 值字段按生产序列化丢弃):
988
- // chatMode 必须显式在场(one-shot=false)——renderer isDone 判据依赖它。
989
- expect(Object.keys(data).sort()).toEqual([
990
- "agent", "chatMode", "depth", "displayItems", "eventLog",
991
- "id", "mode", "model", "rootSessionId", "round", "startedAt",
992
- "status", "task", "totalTokens", "turns", "v", "worktree",
993
- ]);
994
- expect(data).toMatchObject({
995
- v: 1,
996
- id: "r1",
997
- agent: "worker",
998
- task: "t",
999
- status: "running",
1000
- mode: "sync",
1001
- startedAt: 1000,
1002
- rootSessionId: "sess-current",
1003
- chatMode: false,
1004
- turns: 0,
1005
- totalTokens: 0,
1006
- model: "m",
1007
- eventLog: [],
1008
- displayItems: [],
1009
- });
1010
- });
1011
-
1012
- it("archive:append 终态完整快照(result/endedAt/closedReason);one-shot 生命周期共 2 次 append", () => {
1013
- const { store, appended } = makeStoreWithPi();
1014
- const r = makeRecord();
1015
- store.register(r);
1016
- // 模拟正常终态路径:tryTransition CAS → completeRecord 冻结 → archive(D-017 时序)
1017
- tryTransition(r, "closed", "gc");
1018
- completeRecord(r, { text: "task done", turns: 1, durationMs: 500, success: true, sessionId: "r1", toolCalls: [] }, "closed", "gc");
1019
- store.archive(r);
1020
-
1021
- // 探针基线(单测级):one-shot 生命周期 = register + archive = 2 次 append
1022
- expect(appended).toHaveLength(2);
1023
- const data = asEntryData(appended[1]?.data);
1024
- expect(data).toMatchObject({
1025
- v: 1,
1026
- id: "r1",
1027
- status: "closed",
1028
- closedReason: "gc",
1029
- result: "task done",
1030
- endedAt: expect.any(Number) as number,
1031
- });
1032
- });
1033
-
1034
- it("reportRecordTransition:类外恢复写点上报(chatMode 续轮 round 携带)", () => {
1035
- const { store, appended } = makeStoreWithPi();
1036
- const r = makeRecord({ chatMode: true, round: 1 });
1037
- store.reportRecordTransition(r);
1038
-
1039
- expect(appended).toHaveLength(1);
1040
- expect(appended[0]?.customType).toBe(SUBAGENT_RECORD_CUSTOM_TYPE);
1041
- expect(asEntryData(appended[0]?.data)).toMatchObject({
1042
- v: 1,
1043
- id: "r1",
1044
- status: "running",
1045
- round: 1,
1046
- });
1047
- });
1048
-
1049
- it("pi 未注入(session_start 前):三写点均安全降级不抛错", () => {
1050
- const store = new RecordStore(tmpDir);
1051
- const r = makeRecord();
1052
- expect(() => store.register(r)).not.toThrow();
1053
- expect(() => store.archive(r)).not.toThrow();
1054
- expect(() => store.reportRecordTransition(r)).not.toThrow();
1055
- });
1056
- });
1057
- });