@zhushanwen/pi-subagent-workflow 8.6.0 → 8.8.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 (364) hide show
  1. package/README.md +5 -11
  2. package/package.json +13 -20
  3. package/relay/relay.mjs +1 -1
  4. package/skills/workflow-script-format/SKILL.md +32 -13
  5. package/src/host/__tests__/pi-host.test.ts +235 -0
  6. package/src/host/pi-host.ts +163 -0
  7. package/src/index.ts +141 -138
  8. package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
  9. package/src/injectors/__tests__/engine-section-stability.test.ts +343 -0
  10. package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
  11. package/src/injectors/__tests__/subagent-list-injector.test.ts +59 -19
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +31 -17
  13. package/src/injectors/engine-awareness.ts +119 -0
  14. package/src/injectors/model-list-injector.ts +15 -58
  15. package/src/injectors/subagent-list-injector.ts +56 -114
  16. package/src/injectors/workflow-list-injector.ts +37 -70
  17. package/src/interface/__tests__/detectors.test.ts +48 -37
  18. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +2 -2
  19. package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
  20. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  21. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  22. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
  23. package/src/interface/bg-notify-render.ts +7 -17
  24. package/src/interface/command-actions.ts +5 -15
  25. package/src/interface/commands.ts +4 -4
  26. package/src/interface/format.ts +17 -6
  27. package/src/interface/gui-mappers.ts +18 -22
  28. package/src/interface/helpers.ts +9 -130
  29. package/src/interface/list-component.ts +5 -5
  30. package/src/interface/list-shared.ts +3 -3
  31. package/src/interface/list-view.ts +3 -3
  32. package/src/interface/subagent-actions.ts +66 -439
  33. package/src/interface/subagent-tool-schema.ts +21 -17
  34. package/src/interface/subagent-tool.ts +12 -7
  35. package/src/interface/subagents.ts +4 -4
  36. package/src/interface/tool-render.ts +37 -15
  37. package/src/interface/tool-workflow-script.ts +34 -76
  38. package/src/interface/tool-workflow.ts +57 -94
  39. package/src/interface/views/WorkflowsView.ts +8 -16
  40. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  41. package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
  42. package/src/interface/views/detail-content.ts +4 -4
  43. package/src/interface/views/format.ts +23 -62
  44. package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +137 -225
  45. package/agents/analyst.md +0 -61
  46. package/agents/coder.md +0 -70
  47. package/agents/debugger.md +0 -67
  48. package/agents/doc-reviewer.md +0 -50
  49. package/agents/explorer.md +0 -64
  50. package/agents/general-purpose.md +0 -32
  51. package/agents/orchestrator.md +0 -63
  52. package/agents/planner.md +0 -54
  53. package/agents/researcher.md +0 -65
  54. package/agents/reviewer.md +0 -74
  55. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
  56. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
  57. package/src/execution/__tests__/agent-registry.test.ts +0 -363
  58. package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
  59. package/src/execution/__tests__/alive-store.test.ts +0 -147
  60. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  61. package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
  62. package/src/execution/__tests__/bg-notify-render.test.ts +0 -329
  63. package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
  64. package/src/execution/__tests__/chat-engine-routing.test.ts +0 -601
  65. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
  66. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
  67. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
  68. package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
  69. package/src/execution/__tests__/config.test.ts +0 -110
  70. package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
  71. package/src/execution/__tests__/crash-recovery.test.ts +0 -320
  72. package/src/execution/__tests__/delivery-methods.test.ts +0 -422
  73. package/src/execution/__tests__/dialog-queue.test.ts +0 -299
  74. package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
  75. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
  76. package/src/execution/__tests__/execute-nesting.test.ts +0 -323
  77. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
  78. package/src/execution/__tests__/execution-record.test.ts +0 -1394
  79. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +0 -171
  80. package/src/execution/__tests__/finalize-record.test.ts +0 -368
  81. package/src/execution/__tests__/finalized-marker.test.ts +0 -82
  82. package/src/execution/__tests__/format-schema-instruction.test.ts +0 -169
  83. package/src/execution/__tests__/format.test.ts +0 -458
  84. package/src/execution/__tests__/gc-timer.test.ts +0 -184
  85. package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
  86. package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
  87. package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
  88. package/src/execution/__tests__/helpers/spawn-mock.ts +0 -242
  89. package/src/execution/__tests__/host-mode.test.ts +0 -87
  90. package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
  91. package/src/execution/__tests__/index-session-start.test.ts +0 -672
  92. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  93. package/src/execution/__tests__/lifecycle-manager.test.ts +0 -383
  94. package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
  95. package/src/execution/__tests__/list-component.test.ts +0 -401
  96. package/src/execution/__tests__/list-fields.test.ts +0 -140
  97. package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
  98. package/src/execution/__tests__/model-resolver.test.ts +0 -465
  99. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
  100. package/src/execution/__tests__/nested-visibility.test.ts +0 -325
  101. package/src/execution/__tests__/notifier-flush.test.ts +0 -612
  102. package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
  103. package/src/execution/__tests__/notify-ledger.test.ts +0 -826
  104. package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
  105. package/src/execution/__tests__/output-collector.test.ts +0 -358
  106. package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
  107. package/src/execution/__tests__/path-encoding.test.ts +0 -104
  108. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  109. package/src/execution/__tests__/record-store.test.ts +0 -1057
  110. package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
  111. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
  112. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
  113. package/src/execution/__tests__/relay-agent.test.ts +0 -448
  114. package/src/execution/__tests__/relay-env.test.ts +0 -42
  115. package/src/execution/__tests__/resource-policy.test.ts +0 -109
  116. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  117. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
  118. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
  119. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -687
  120. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  121. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  122. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  123. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -199
  124. package/src/execution/__tests__/sdk-contract.test.ts +0 -273
  125. package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
  126. package/src/execution/__tests__/session-file-gc.test.ts +0 -293
  127. package/src/execution/__tests__/session-pending.test.ts +0 -197
  128. package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
  129. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
  130. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -350
  131. package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
  132. package/src/execution/__tests__/spawn-args.test.ts +0 -445
  133. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
  134. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  135. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
  136. package/src/execution/__tests__/spawned-children.test.ts +0 -92
  137. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  138. package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
  139. package/src/execution/__tests__/status-refactor.test.ts +0 -345
  140. package/src/execution/__tests__/stdin-writer.test.ts +0 -463
  141. package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
  142. package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
  143. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
  144. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
  145. package/src/execution/__tests__/subagent-service.test.ts +0 -788
  146. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
  147. package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -599
  148. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  149. package/src/execution/__tests__/timeout-integration.test.ts +0 -615
  150. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  151. package/src/execution/__tests__/tool-action.test.ts +0 -476
  152. package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
  153. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  154. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  155. package/src/execution/__tests__/ui-channels.test.ts +0 -187
  156. package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
  157. package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
  158. package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
  159. package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
  160. package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
  161. package/src/execution/__tests__/worktree-manager.test.ts +0 -633
  162. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
  163. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
  164. package/src/execution/__tests__/worktree-registry.test.ts +0 -199
  165. package/src/execution/agent-registry.ts +0 -199
  166. package/src/execution/agent-result-mapper.ts +0 -90
  167. package/src/execution/alive-store.ts +0 -92
  168. package/src/execution/argv-mirror.ts +0 -113
  169. package/src/execution/best-effort.ts +0 -38
  170. package/src/execution/channel-registry-access.ts +0 -144
  171. package/src/execution/concurrency-pool.ts +0 -116
  172. package/src/execution/config.ts +0 -96
  173. package/src/execution/dialog-queue.ts +0 -330
  174. package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
  175. package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
  176. package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
  177. package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
  178. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
  179. package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
  180. package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
  181. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  182. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  183. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
  184. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
  185. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
  186. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
  187. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
  188. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
  189. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
  190. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
  191. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
  192. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
  193. package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
  194. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
  195. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  196. package/src/execution/engine/__tests__/registry.test.ts +0 -120
  197. package/src/execution/engine/__tests__/routing.test.ts +0 -231
  198. package/src/execution/engine/common/data-dir.ts +0 -62
  199. package/src/execution/engine/common/errors.ts +0 -183
  200. package/src/execution/engine/common/event-journal.ts +0 -254
  201. package/src/execution/engine/common/journal-replay.ts +0 -62
  202. package/src/execution/engine/common/kill-chain.ts +0 -221
  203. package/src/execution/engine/common/nesting-guard.ts +0 -50
  204. package/src/execution/engine/common/persona-router.ts +0 -108
  205. package/src/execution/engine/common/pool-manager.ts +0 -226
  206. package/src/execution/engine/common/schema-emulation.ts +0 -189
  207. package/src/execution/engine/common/session-view-projection.ts +0 -51
  208. package/src/execution/engine/engine-discovery.ts +0 -65
  209. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
  210. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  211. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  212. package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
  213. package/src/execution/engine/engines/pi/reader.ts +0 -48
  214. package/src/execution/engine/engines/pi/registration.ts +0 -35
  215. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  216. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  217. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  218. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
  219. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
  220. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  221. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
  222. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
  223. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
  224. package/src/execution/engine/engines/zcode/constants.ts +0 -43
  225. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
  226. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  227. package/src/execution/engine/engines/zcode/parser.ts +0 -436
  228. package/src/execution/engine/engines/zcode/preparer.ts +0 -363
  229. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  230. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  231. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -658
  232. package/src/execution/engine/host-task-spec.ts +0 -47
  233. package/src/execution/engine/model-prompt.ts +0 -59
  234. package/src/execution/engine/paths.ts +0 -42
  235. package/src/execution/engine/port.ts +0 -153
  236. package/src/execution/engine/registry.ts +0 -123
  237. package/src/execution/engine/routing.ts +0 -218
  238. package/src/execution/engine/types.ts +0 -309
  239. package/src/execution/execute-options-mapper.ts +0 -112
  240. package/src/execution/execution-record.ts +0 -965
  241. package/src/execution/finalize-record.ts +0 -251
  242. package/src/execution/finalized-marker.ts +0 -51
  243. package/src/execution/get-state-handshake.ts +0 -104
  244. package/src/execution/host-mode.ts +0 -56
  245. package/src/execution/idle-gc.ts +0 -47
  246. package/src/execution/lifecycle-manager.ts +0 -513
  247. package/src/execution/lifecycle-predicates.ts +0 -65
  248. package/src/execution/manifest-store.ts +0 -256
  249. package/src/execution/model-config-service.ts +0 -219
  250. package/src/execution/model-resolver.ts +0 -248
  251. package/src/execution/notifier.ts +0 -330
  252. package/src/execution/notify-ledger.ts +0 -580
  253. package/src/execution/output-collector.ts +0 -228
  254. package/src/execution/path-encoding.ts +0 -60
  255. package/src/execution/pi-invocation.ts +0 -120
  256. package/src/execution/record-entry.ts +0 -132
  257. package/src/execution/record-store.ts +0 -1234
  258. package/src/execution/relay-env.ts +0 -37
  259. package/src/execution/session-context-resolver.ts +0 -64
  260. package/src/execution/session-file-gc.ts +0 -120
  261. package/src/execution/session-pending.ts +0 -164
  262. package/src/execution/session-reconstructor.ts +0 -678
  263. package/src/execution/session-runner.ts +0 -1781
  264. package/src/execution/sessions-index.ts +0 -304
  265. package/src/execution/spawn-event-adapter.ts +0 -363
  266. package/src/execution/stdin-writer.ts +0 -198
  267. package/src/execution/stream-sink.ts +0 -126
  268. package/src/execution/subagent-service.ts +0 -2141
  269. package/src/execution/subprocess-agent-runner.ts +0 -317
  270. package/src/execution/temp-prompt.ts +0 -62
  271. package/src/execution/tombstone-store.ts +0 -72
  272. package/src/execution/turn-limiter.ts +0 -102
  273. package/src/execution/types.ts +0 -973
  274. package/src/execution/ui-channels.ts +0 -216
  275. package/src/execution/ui-interaction-model.ts +0 -48
  276. package/src/execution/ui-request-handler-factory.ts +0 -212
  277. package/src/execution/ui-request-observability.ts +0 -81
  278. package/src/execution/ui-request-queue.ts +0 -184
  279. package/src/execution/worktree-manager.ts +0 -688
  280. package/src/execution/worktree-registry.ts +0 -222
  281. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -349
  282. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -202
  283. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -152
  284. package/src/orchestration/__tests__/args-validator.test.ts +0 -143
  285. package/src/orchestration/__tests__/config-loader.test.ts +0 -503
  286. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -809
  287. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
  288. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
  289. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
  290. package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -451
  291. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +0 -150
  292. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
  293. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +0 -202
  294. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
  295. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -600
  296. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
  297. package/src/orchestration/__tests__/lifecycle.test.ts +0 -659
  298. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +0 -95
  299. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -2025
  300. package/src/orchestration/__tests__/script-lint.test.ts +0 -831
  301. package/src/orchestration/__tests__/skill-discovery.test.ts +0 -201
  302. package/src/orchestration/__tests__/test-mocks.ts +0 -197
  303. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +0 -368
  304. package/src/orchestration/__tests__/worker-host.test.ts +0 -120
  305. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
  306. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -563
  307. package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
  308. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -129
  309. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
  310. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
  311. package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
  312. package/src/orchestration/agent-opts-resolver.ts +0 -174
  313. package/src/orchestration/args-validator.ts +0 -127
  314. package/src/orchestration/config-loader.ts +0 -315
  315. package/src/orchestration/error-recovery.ts +0 -1018
  316. package/src/orchestration/execute-agent-call.ts +0 -256
  317. package/src/orchestration/launcher.ts +0 -455
  318. package/src/orchestration/lifecycle.ts +0 -399
  319. package/src/orchestration/models/__tests__/budget.test.ts +0 -307
  320. package/src/orchestration/models/__tests__/trace.test.ts +0 -408
  321. package/src/orchestration/models/agent-call.ts +0 -83
  322. package/src/orchestration/models/budget.ts +0 -118
  323. package/src/orchestration/models/ports.ts +0 -164
  324. package/src/orchestration/models/run-runtime.ts +0 -104
  325. package/src/orchestration/models/run-spec.ts +0 -83
  326. package/src/orchestration/models/run-state.ts +0 -44
  327. package/src/orchestration/models/trace.ts +0 -183
  328. package/src/orchestration/models/types.ts +0 -301
  329. package/src/orchestration/models/workflow-run.ts +0 -254
  330. package/src/orchestration/models/workflow-script-registry.ts +0 -35
  331. package/src/orchestration/models/workflow-script.ts +0 -117
  332. package/src/orchestration/script-lint.ts +0 -766
  333. package/src/orchestration/skill-discovery.ts +0 -115
  334. package/src/orchestration/worker-handle.ts +0 -115
  335. package/src/orchestration/worker-host.ts +0 -98
  336. package/src/orchestration/worker-script-builder.ts +0 -421
  337. package/src/orchestration/workflow-files.ts +0 -85
  338. package/src/orchestration/workflow-script-registry-impl.ts +0 -133
  339. package/src/shared/__tests__/agent-ref.test.ts +0 -34
  340. package/src/shared/__tests__/meta-parser.test.ts +0 -304
  341. package/src/shared/__tests__/model-ref.test.ts +0 -306
  342. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
  343. package/src/shared/__tests__/resource-discovery.test.ts +0 -736
  344. package/src/shared/__tests__/resource-meta.test.ts +0 -51
  345. package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
  346. package/src/shared/__tests__/timer-delay.test.ts +0 -61
  347. package/src/shared/agent-event.ts +0 -13
  348. package/src/shared/agent-ref.ts +0 -57
  349. package/src/shared/meta-parser.ts +0 -261
  350. package/src/shared/model-ref.ts +0 -286
  351. package/src/shared/resource-discovery.ts +0 -811
  352. package/src/shared/resource-meta.ts +0 -65
  353. package/src/shared/schema-env.ts +0 -44
  354. package/src/shared/schema-jsonify.ts +0 -58
  355. package/src/shared/timer-delay.ts +0 -54
  356. package/src/shared/xml-injection.ts +0 -35
  357. package/workflows/README.md +0 -81
  358. package/workflows/_shared/agent-refs.cjs +0 -40
  359. package/workflows/chain.js +0 -131
  360. package/workflows/map-reduce.js +0 -174
  361. package/workflows/parallel.js +0 -148
  362. package/workflows/review-fix-loop-utils.cjs +0 -1309
  363. package/workflows/review-fix-loop.js +0 -1404
  364. 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
- });