@zhushanwen/subagent-core 0.5.1 → 0.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 (318) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-Dv4QhSJ_.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +835 -668
  6. package/dist/{types-Dv4QhSJ_.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +835 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14123 -15467
  20. package/dist/index.d.cts +1179 -1540
  21. package/dist/index.d.ts +1179 -1540
  22. package/dist/index.js +7694 -10986
  23. package/dist.bundle/index.cjs +33485 -0
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/agent-registry.test.ts +1 -1
  30. package/src/execution/__tests__/alive-store.test.ts +1 -1
  31. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  32. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  33. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  34. package/src/execution/__tests__/collect-coordinator-service.test.ts +316 -0
  35. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  36. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +229 -0
  37. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  38. package/src/execution/__tests__/config.test.ts +1 -1
  39. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  40. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  41. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  42. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +21 -34
  43. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  44. package/src/execution/__tests__/gc-timer.test.ts +8 -23
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  46. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  47. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  48. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  49. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  50. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  52. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  53. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  54. package/src/execution/__tests__/notify-ledger.test.ts +27 -0
  55. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  56. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  57. package/src/execution/__tests__/record-store.test.ts +60 -6
  58. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  59. package/src/execution/__tests__/relay-env.test.ts +12 -1
  60. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  61. package/src/execution/__tests__/session-file-gc.test.ts +1 -1
  62. package/src/execution/__tests__/session-pending.test.ts +119 -17
  63. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  64. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  65. package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
  66. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +14 -11
  67. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  69. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  70. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  71. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  72. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  73. package/src/execution/__tests__/sync-collect-recovery.test.ts +1102 -0
  74. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  75. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  76. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  77. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  78. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  79. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  80. package/src/execution/agent-registry.ts +6 -2
  81. package/src/execution/collect-coordinator.ts +200 -0
  82. package/src/execution/config.ts +77 -5
  83. package/src/execution/dialog-queue.ts +6 -73
  84. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  85. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  86. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  87. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  88. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  89. package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
  90. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  91. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  92. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  93. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  94. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  95. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  96. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  97. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  98. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  99. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +82 -55
  100. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  101. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  102. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  103. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +8 -3
  104. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  105. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  106. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  107. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  108. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  109. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  110. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  111. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  112. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  113. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  114. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  115. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  116. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  117. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  118. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  119. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  120. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  121. package/src/execution/engine/client/client-options.ts +103 -0
  122. package/src/execution/engine/client/engine-client.ts +687 -0
  123. package/src/execution/engine/client/mirror.ts +167 -0
  124. package/src/execution/engine/client/pid-file.ts +392 -0
  125. package/src/execution/engine/client/reaper.ts +95 -0
  126. package/src/execution/engine/client/remote-engine.ts +441 -0
  127. package/src/execution/engine/client/reverse-router.ts +255 -0
  128. package/src/execution/engine/common/capability-gate.ts +109 -22
  129. package/src/execution/engine/common/errors.ts +9 -25
  130. package/src/execution/engine/common/event-journal.ts +4 -50
  131. package/src/execution/engine/common/journal-replay.ts +3 -1
  132. package/src/execution/engine/common/kill-chain.ts +18 -251
  133. package/src/execution/engine/common/nesting-guard.ts +18 -114
  134. package/src/execution/engine/common/pool-manager.ts +4 -0
  135. package/src/execution/engine/common/session-view-service.ts +56 -84
  136. package/src/execution/engine/config.ts +151 -0
  137. package/src/execution/engine/d8-compat.ts +346 -0
  138. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  139. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  140. package/src/execution/engine/engine-discovery.ts +30 -7
  141. package/src/execution/engine/engine-inspect-package.ts +282 -0
  142. package/src/execution/engine/engine-manifest.ts +244 -0
  143. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  144. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  145. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  146. package/src/execution/engine/host/host-bridge.ts +150 -0
  147. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  148. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  149. package/src/execution/engine/host/spawned-children.ts +139 -0
  150. package/src/execution/engine/host-task-spec.ts +29 -2
  151. package/src/execution/engine/model-validation.ts +55 -2
  152. package/src/execution/engine/paths.ts +16 -41
  153. package/src/execution/engine/port.ts +48 -2
  154. package/src/execution/engine/registry.ts +153 -34
  155. package/src/execution/engine/routing.ts +225 -52
  156. package/src/execution/engine/types.ts +25 -200
  157. package/src/execution/execution-record.ts +7 -0
  158. package/src/execution/finalize-record.ts +40 -11
  159. package/src/execution/idle-gc.ts +89 -14
  160. package/src/execution/lifecycle-manager.ts +10 -7
  161. package/src/execution/lifecycle-predicates.ts +10 -8
  162. package/src/execution/notifier.ts +234 -25
  163. package/src/execution/notify-host.ts +21 -1
  164. package/src/execution/record-entry.ts +16 -0
  165. package/src/execution/record-store.ts +189 -31
  166. package/src/execution/relay-env.ts +19 -36
  167. package/src/execution/round-supervisor/domain.ts +89 -0
  168. package/src/execution/round-supervisor/index.ts +33 -0
  169. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  170. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  171. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  172. package/src/execution/round-supervisor/service-binding.ts +273 -0
  173. package/src/execution/round-supervisor/supervisor.ts +426 -0
  174. package/src/execution/session-pending.ts +68 -74
  175. package/src/execution/settled-watchdog.ts +39 -0
  176. package/src/execution/subagent-actions-core.ts +62 -6
  177. package/src/execution/subagent-service.ts +1161 -406
  178. package/src/execution/subprocess-agent-runner.ts +16 -8
  179. package/src/execution/sync-rebuild.ts +87 -0
  180. package/src/execution/types.ts +104 -117
  181. package/src/execution/ui-channels.ts +13 -214
  182. package/src/execution/ui-request-handler-factory.ts +7 -4
  183. package/src/execution/ui-request-observability.ts +2 -2
  184. package/src/execution/workflow-state-root.ts +67 -0
  185. package/src/execution/worktree-git-ops.ts +5 -0
  186. package/src/execution/worktree-manager.ts +11 -1
  187. package/src/index.ts +55 -26
  188. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  189. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  190. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  191. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  192. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  193. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  194. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  195. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  196. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  197. package/src/orchestration/execute-agent-call.ts +1 -1
  198. package/src/orchestration/file-run-store.ts +144 -59
  199. package/src/orchestration/models/types.ts +43 -42
  200. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  201. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
  202. package/src/shared/zcode-model-ref.ts +31 -0
  203. package/dist/chunk-43ONBFZX.js +0 -240
  204. package/dist/chunk-A4IVZWVX.js +0 -31
  205. package/dist/chunk-APZY4IME.js +0 -27
  206. package/dist/chunk-V3VHZ2VX.js +0 -59
  207. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  208. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  209. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  210. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  211. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  212. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  213. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  214. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  215. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  216. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  217. package/src/__tests__/session-runner.test.ts +0 -69
  218. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  219. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  220. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  221. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  222. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  223. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  224. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  225. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  226. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  227. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  228. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  229. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  230. package/src/execution/__tests__/output-collector.test.ts +0 -469
  231. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  232. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  233. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  234. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  235. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  236. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  237. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  238. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  239. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  240. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  241. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  242. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  243. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  244. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  245. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  246. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  247. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  248. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  249. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  250. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  251. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  252. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  253. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  254. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  255. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  256. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  257. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  258. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  259. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  260. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  261. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  262. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  263. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  264. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  265. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  266. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  267. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  268. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  269. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  270. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  271. package/src/execution/engine/common/schema-emulation.ts +0 -189
  272. package/src/execution/engine/common/session-view-projection.ts +0 -51
  273. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  274. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  275. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  276. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  277. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  278. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  279. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  280. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  281. package/src/execution/engine/engines/pi/reader.ts +0 -59
  282. package/src/execution/engine/engines/pi/registration.ts +0 -37
  283. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  284. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  285. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  286. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  287. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  288. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  289. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  290. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  291. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  292. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  293. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  294. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  295. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  296. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  297. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  298. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  299. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  300. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  301. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  302. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  303. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  304. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  305. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  306. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  307. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  308. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  309. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  310. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  311. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  312. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  313. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  314. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  315. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  316. package/src/execution/round-settlement.ts +0 -93
  317. package/src/execution/ui-request-queue.ts +0 -215
  318. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,500 @@
1
+ // src/execution/__tests__/notify-batch.test.ts
2
+ //
3
+ // notifier.notifyBatch 单测族(subagent-sync-collect U3):
4
+ // 1. buildBatchNotifyId:幂等键 sync-batch:<sha1(sorted ids)>——同成员集不同登记
5
+ // 顺序同 hash(D2 隐式批身份稳定性),不同成员集异 hash;
6
+ // 2. buildBatchLlmContent:批头计数(N finished / M failed / K cancelled,A3/A8
7
+ // 前置)+ 批头行与成员条目 "\n\n---\n\n" join 形态;
8
+ // 3. notifyBatch + fake ledger:单 entry 写账 + notifyId 正确 + details 批形态 +
9
+ // attemptDeliver 边沿投递(与 notify 同一四步链);
10
+ // 4. notifyBatch 幂等:同成员集二次投递被账本拒绝(零副作用);
11
+ // 5. notifyBatch 无 ledger 降级:内核路径单条 send(dedupeKey = 批 hash);
12
+ // 6. CollectCoordinator ↔ notifyBatch 集成:跨轮续累(D2 探针)——分两轮 route
13
+ // 2+1 sync → 闭合时单批 3 成员一次投递。
14
+ //
15
+ // fake ledger host 手法对齐 notify-ledger.test.ts(同款 mock host + settled 回调);
16
+ // 全程 tmpdir 零文件 IO(本族纯内存);env 剥离见 beforeEach(RELAY/PI_SUBAGENT
17
+ // 泄漏防护,impl-plan 偏差#5 同族)。
18
+
19
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
+
21
+ const { loggerMock } = vi.hoisted(() => ({
22
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
23
+ }));
24
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
25
+
26
+ import {
27
+ bindNotifyLedgerHost,
28
+ NOTIFY_CUSTOM_TYPE,
29
+ NOTIFY_LEDGER_CUSTOM_TYPE,
30
+ _resetNotifyLedgerForTest,
31
+ type NotifyLedgerHost,
32
+ } from "../notify-ledger.ts";
33
+ import {
34
+ buildBatchLlmContent,
35
+ buildBatchNotifyId,
36
+ createNotifier,
37
+ type BgNotifyRecord,
38
+ type NotifierHost,
39
+ } from "../notifier.ts";
40
+ import { CollectCoordinator } from "../collect-coordinator.ts";
41
+ import type { ExecutionRecord, SubagentRecord } from "../types.ts";
42
+
43
+ // ─── env 剥离(impl-plan 偏差#5:RELAY/PI_SUBAGENT 泄漏即红的存量敏感面隔离)──
44
+
45
+ const STRIPPED_ENV_PREFIXES = ["XYZ_SUBAGENT_RELAY_", "PI_SUBAGENT_"] as const;
46
+
47
+ function stripSensitiveEnv(): void {
48
+ for (const key of Object.keys(process.env)) {
49
+ if (STRIPPED_ENV_PREFIXES.some((p) => key.startsWith(p))) delete process.env[key];
50
+ }
51
+ }
52
+
53
+ // ─── fake ledger host(notify-ledger.test.ts 同款)──
54
+
55
+ interface LedgerHostMock {
56
+ host: NotifyLedgerHost;
57
+ // appendLedgerEntry(type/customType/data)与 sendDelivery(customType/content/display/details)
58
+ // 两种 entry 形态共用一个收集数组,宽化为字段并集
59
+ entries: { type: string; customType: string; data?: Record<string, unknown>; content?: string; display?: boolean; details?: unknown }[];
60
+ sentMessages: { customType: string; content: string; display: boolean; details?: unknown }[];
61
+ settledHandlers: Array<() => void>;
62
+ setIdle(idle: boolean): void;
63
+ }
64
+
65
+ function makeLedgerHost(): LedgerHostMock {
66
+ const entries: LedgerHostMock["entries"] = [];
67
+ const sentMessages: LedgerHostMock["sentMessages"] = [];
68
+ const settledHandlers: Array<() => void> = [];
69
+ const idle = { value: true };
70
+ return {
71
+ host: {
72
+ appendLedgerEntry: (customType, data) => {
73
+ entries.push({ type: "custom", customType, data: data as Record<string, unknown> });
74
+ },
75
+ readSessionEntries: () => entries,
76
+ isIdle: () => idle.value,
77
+ onAgentSettled: (handler) => {
78
+ settledHandlers.push(handler);
79
+ },
80
+ sendDelivery: (message) => {
81
+ sentMessages.push(message);
82
+ entries.push({
83
+ type: "custom_message",
84
+ customType: message.customType,
85
+ content: message.content,
86
+ display: message.display,
87
+ details: message.details as Record<string, unknown>,
88
+ });
89
+ },
90
+ },
91
+ entries,
92
+ sentMessages,
93
+ settledHandlers,
94
+ setIdle: (v: boolean) => {
95
+ idle.value = v;
96
+ },
97
+ };
98
+ }
99
+
100
+ // ─── 成员快照工厂 ──
101
+
102
+ function makeMember(id: string, over: Partial<BgNotifyRecord> = {}): BgNotifyRecord {
103
+ return {
104
+ id,
105
+ status: "closed",
106
+ agent: `worker-${id}`,
107
+ result: `result of ${id}`,
108
+ startedAt: 1000,
109
+ endedAt: 2000,
110
+ ...over,
111
+ };
112
+ }
113
+
114
+ // ─── 协调器集成 harness(collect-coordinator.test.ts 同款注入风格)──
115
+
116
+ function makeExecutionRecord(id: string, collectMode: "sync" | undefined): ExecutionRecord {
117
+ return {
118
+ id,
119
+ agent: "/agents/worker.md",
120
+ model: "prov/m1",
121
+ thinkingLevel: undefined,
122
+ mode: "background",
123
+ task: "t",
124
+ slug: "worker",
125
+ startedAt: 1000,
126
+ rootSessionId: "root-A",
127
+ parentRecordId: undefined,
128
+ depth: 0,
129
+ status: "closed",
130
+ turns: [],
131
+ turnCount: 1,
132
+ totalTokens: 10,
133
+ lastError: undefined,
134
+ endedAt: 2000,
135
+ result: `result of ${id}`,
136
+ error: undefined,
137
+ agentResult: undefined,
138
+ controller: undefined,
139
+ collectMode,
140
+ } as ExecutionRecord;
141
+ }
142
+
143
+ function makeStoreRec(id: string, collectMode: "sync" | undefined, status: "closed" | "running"): SubagentRecord {
144
+ return {
145
+ id,
146
+ agent: "/agents/worker.md",
147
+ task: "t",
148
+ slug: "worker",
149
+ status,
150
+ mode: "background",
151
+ startedAt: 1000,
152
+ rootSessionId: "root-A",
153
+ parentRecordId: undefined,
154
+ depth: 0,
155
+ endedAt: status === "closed" ? 2000 : undefined,
156
+ turns: 1,
157
+ totalTokens: 10,
158
+ model: "prov/m1",
159
+ thinkingLevel: undefined,
160
+ eventLog: [],
161
+ displayItems: [],
162
+ collectMode,
163
+ } as SubagentRecord;
164
+ }
165
+
166
+ beforeEach(() => {
167
+ stripSensitiveEnv();
168
+ });
169
+
170
+ afterEach(() => {
171
+ _resetNotifyLedgerForTest();
172
+ });
173
+
174
+ // ─── 1. 批身份键 ──
175
+
176
+ describe("buildBatchNotifyId — sync-batch:<sha1(sorted ids)>", () => {
177
+ it("同成员集不同登记顺序同 hash(D2 隐式批身份稳定性)", () => {
178
+ const a = buildBatchNotifyId(["sa-c", "sa-a", "sa-b"]);
179
+ const b = buildBatchNotifyId(["sa-b", "sa-c", "sa-a"]);
180
+ const c = buildBatchNotifyId(["sa-a", "sa-b", "sa-c"]);
181
+ expect(a).toBe(b);
182
+ expect(b).toBe(c);
183
+ });
184
+
185
+ it("前缀 sync-batch: + 不同成员集异 hash(跨批不互吞)", () => {
186
+ const id = buildBatchNotifyId(["sa-1", "sa-2"]);
187
+ expect(id.startsWith("sync-batch:")).toBe(true);
188
+ expect(id).not.toBe(buildBatchNotifyId(["sa-1", "sa-3"]));
189
+ // 单成员批也是合法批(后派 async 与残留单 sync 的混派边界)
190
+ expect(buildBatchNotifyId(["sa-1"])).not.toBe(buildBatchNotifyId(["sa-2"]));
191
+ });
192
+ });
193
+
194
+ // ─── 2. 批头计数 + join 形态 ──
195
+
196
+ describe("buildBatchLlmContent — 批头计数与 join 形态(A3/A8 前置)", () => {
197
+ it("3 finished:批头 `3 finished, 0 failed, 0 cancelled.` + 三条目 join", () => {
198
+ const members = [makeMember("sa-1"), makeMember("sa-2"), makeMember("sa-3")];
199
+ const content = buildBatchLlmContent(members);
200
+ const lines = content.split("\n\n---\n\n");
201
+ expect(lines).toHaveLength(4);
202
+ expect(lines[0]).toBe("Subagent batch completed: 3 finished, 0 failed, 0 cancelled.");
203
+ expect(lines[1]).toBe('Subagent "worker-sa-1" (sa-1) completed. Result:\nresult of sa-1');
204
+ expect(lines[3]).toBe('Subagent "worker-sa-3" (sa-3) completed. Result:\nresult of sa-3');
205
+ });
206
+
207
+ it("1 failed + 2 finished:失败条目 error 内联全文形态(A3,多行 error 原样保留)", () => {
208
+ const members = [
209
+ makeMember("sa-ok-1"),
210
+ makeMember("sa-bad", { result: undefined, error: "spawn EPIPE\ndetail line" }),
211
+ makeMember("sa-ok-2"),
212
+ ];
213
+ const content = buildBatchLlmContent(members);
214
+ expect(content).toContain("Subagent batch completed: 2 finished, 1 failed, 0 cancelled.");
215
+ expect(content).toContain('Subagent "worker-sa-bad" (sa-bad) failed: spawn EPIPE\ndetail line');
216
+ });
217
+
218
+ it("cancelled 计数:cancelled 条目短文案(E6 cancel 入批)", () => {
219
+ const members = [
220
+ makeMember("sa-ok"),
221
+ makeMember("sa-gone", { result: undefined, closedReason: "cancelled" }),
222
+ ];
223
+ const content = buildBatchLlmContent(members);
224
+ expect(content).toContain("Subagent batch completed: 1 finished, 0 failed, 1 cancelled.");
225
+ expect(content).toContain('Subagent "worker-sa-gone" (sa-gone) cancelled.');
226
+ });
227
+
228
+ it("outcome 已物化的成员直接读(不二次派生,计数与条目同源)", () => {
229
+ const members = [makeMember("sa-1", { outcome: "failed", error: "boom" })];
230
+ const content = buildBatchLlmContent(members);
231
+ expect(content).toContain("Subagent batch completed: 0 finished, 1 failed, 0 cancelled.");
232
+ });
233
+ });
234
+
235
+ // ─── 3+4. notifyBatch × ledger 四步链 + 幂等 ──
236
+
237
+ describe("notifyBatch — ledger 写账 → 边沿投递(与 notify 同一通道,单 entry)", () => {
238
+ it("闭合投递:单 entry 写账 + notifyId = sync-batch hash + details 批形态(顶层 notifyId 可达)", () => {
239
+ const mock = makeLedgerHost();
240
+ bindNotifyLedgerHost(mock.host);
241
+ const notifier = createNotifier({
242
+ sendMessage: () => {},
243
+ hasRunningBackground: () => false,
244
+ isIdle: () => true,
245
+ });
246
+
247
+ const members = [makeMember("sa-b"), makeMember("sa-a")];
248
+ const expectedId = buildBatchNotifyId(["sa-b", "sa-a"]);
249
+ const accepted = notifier.notifyBatch(members);
250
+ expect(accepted).toBe(true);
251
+
252
+ // 单 entry 写账:notifyId 为成员集 hash(与登记顺序无关)
253
+ const ledgerEntries = mock.entries.filter((e) => e.customType === NOTIFY_LEDGER_CUSTOM_TYPE);
254
+ expect(ledgerEntries).toHaveLength(1);
255
+ expect(ledgerEntries[0]?.data?.["notifyId"]).toBe(expectedId);
256
+
257
+ // idle → attemptDeliver 立即投递:单条批消息(不与 mergeItems 再包一层——批即单 entry)
258
+ expect(mock.sentMessages).toHaveLength(1);
259
+ const sent = mock.sentMessages[0]!;
260
+ expect(sent.customType).toBe(NOTIFY_CUSTOM_TYPE);
261
+ expect(sent.content).toContain("Subagent batch completed: 2 finished, 0 failed, 0 cancelled.");
262
+ expect(sent.content).toContain('Subagent "worker-sa-a" (sa-a) completed. Result:\nresult of sa-a');
263
+ const details = sent.details as { batch: boolean; notifyId: string; items: BgNotifyRecord[] };
264
+ expect(details.batch).toBe(true);
265
+ // 顶层 notifyId = 回执匹配键(collectDeliveredNotifyIds 认 details.notifyId)
266
+ expect(details.notifyId).toBe(expectedId);
267
+ expect(details.items).toHaveLength(2);
268
+ expect(details.items.map((i) => i.id)).toEqual(["sa-b", "sa-a"]);
269
+ // 成员条目 outcome 物化(bg-notify-render 渲染锁依赖顶层 outcome)
270
+ expect(details.items.every((i) => i.outcome === "completed")).toBe(true);
271
+
272
+ // 回执销账闭环:送达 entry 的 details.notifyId 匹配 → settled 边沿后账面清零
273
+ const fireSettled = () => {
274
+ for (const handler of mock.settledHandlers) handler();
275
+ };
276
+ fireSettled();
277
+ expect(mock.entries.some((e) => e.customType === "subagent-bg-notify-ack" && e.data?.["notifyId"] === expectedId)).toBe(true);
278
+
279
+ notifier.dispose();
280
+ });
281
+
282
+ it("busy 挂 pending,settled 边沿补投(闭合触发即 flush 的 busy 分支)", () => {
283
+ const mock = makeLedgerHost();
284
+ bindNotifyLedgerHost(mock.host);
285
+ mock.setIdle(false);
286
+ const notifier = createNotifier({
287
+ sendMessage: () => {},
288
+ hasRunningBackground: () => false,
289
+ isIdle: () => false,
290
+ });
291
+
292
+ expect(notifier.notifyBatch([makeMember("sa-1")])).toBe(true);
293
+ expect(mock.sentMessages).toHaveLength(0); // busy:账已落,投递挂边沿
294
+ expect(mock.entries.filter((e) => e.customType === NOTIFY_LEDGER_CUSTOM_TYPE)).toHaveLength(1);
295
+
296
+ mock.setIdle(true);
297
+ for (const handler of mock.settledHandlers) handler();
298
+ expect(mock.sentMessages).toHaveLength(1);
299
+ notifier.dispose();
300
+ });
301
+
302
+ it("幂等:同成员集二次 notifyBatch 被账本拒绝(零副作用)——E1 重建重发防线", () => {
303
+ const mock = makeLedgerHost();
304
+ bindNotifyLedgerHost(mock.host);
305
+ mock.setIdle(false); // 挂 pending,隔离投递时机干扰
306
+ const notifier = createNotifier({
307
+ sendMessage: () => {},
308
+ hasRunningBackground: () => false,
309
+ isIdle: () => false,
310
+ });
311
+
312
+ const members = [makeMember("sa-1"), makeMember("sa-2")];
313
+ expect(notifier.notifyBatch(members)).toBe(true);
314
+ const entriesBefore = mock.entries.length;
315
+ // 乱序同成员集 → 同 hash → 拒绝
316
+ expect(notifier.notifyBatch([members[1]!, members[0]!])).toBe(false);
317
+ expect(mock.entries).toHaveLength(entriesBefore);
318
+
319
+ // 异成员集 → 异 hash → 新批独立写账
320
+ expect(notifier.notifyBatch([makeMember("sa-1"), makeMember("sa-3")])).toBe(true);
321
+ const ledgerIds = mock.entries
322
+ .filter((e) => e.customType === NOTIFY_LEDGER_CUSTOM_TYPE)
323
+ .map((e) => e.data?.["notifyId"]);
324
+ expect(new Set(ledgerIds).size).toBe(2);
325
+
326
+ notifier.dispose();
327
+ });
328
+
329
+ it("空批 / dispose 后:false 短路零副作用", () => {
330
+ const mock = makeLedgerHost();
331
+ bindNotifyLedgerHost(mock.host);
332
+ const notifier = createNotifier({
333
+ sendMessage: () => {},
334
+ hasRunningBackground: () => false,
335
+ isIdle: () => true,
336
+ });
337
+ expect(notifier.notifyBatch([])).toBe(false);
338
+ notifier.dispose();
339
+ expect(notifier.notifyBatch([makeMember("sa-1")])).toBe(false);
340
+ expect(mock.entries).toHaveLength(0);
341
+ });
342
+ });
343
+
344
+ // ─── 5. 无 ledger 降级 ──
345
+
346
+ describe("notifyBatch — 无 ledger 内核路径降级(旧装配/无 ledger 测试兼容)", () => {
347
+ it("handle.send 单条批消息,dedupeKey = 批 hash", () => {
348
+ const sent: { content: string; details: unknown }[] = [];
349
+ const notifier = createNotifier({
350
+ sendMessage: (message) => {
351
+ sent.push({ content: (message as { content: string }).content, details: (message as { details?: unknown }).details });
352
+ },
353
+ hasRunningBackground: () => false,
354
+ isIdle: () => true,
355
+ });
356
+ const accepted = notifier.notifyBatch([makeMember("sa-x"), makeMember("sa-y")]);
357
+ expect(accepted).toBe(true);
358
+ expect(loggerMock.warn).toHaveBeenCalledWith(
359
+ expect.stringContaining("notify ledger not bound"),
360
+ expect.objectContaining({ notifyId: buildBatchNotifyId(["sa-x", "sa-y"]) }),
361
+ );
362
+ expect(sent).toHaveLength(1);
363
+ expect(sent[0]!.content).toContain("Subagent batch completed: 2 finished, 0 failed, 0 cancelled.");
364
+ expect((sent[0]!.details as { batch: boolean }).batch).toBe(true);
365
+ notifier.dispose();
366
+ });
367
+ });
368
+
369
+ // ─── 6. 协调器 ↔ notifyBatch 集成(D2 跨轮续累)──
370
+
371
+ describe("CollectCoordinator ↔ notifyBatch 集成 — 跨轮续累单批(D2 探针)", () => {
372
+ /** 真协调器 + 真 notifier(fake ledger host)+ store 枚举 stub:route 全真链。 */
373
+ function makeHarness(storeRecords: SubagentRecord[]) {
374
+ const mock = makeLedgerHost();
375
+ bindNotifyLedgerHost(mock.host);
376
+ const notifier = createNotifier({
377
+ sendMessage: () => {},
378
+ hasRunningBackground: () => false,
379
+ isIdle: () => true,
380
+ });
381
+ const coordinator = new CollectCoordinator({
382
+ notifyAsync: () => {},
383
+ toNotifyRecord: (record) => ({
384
+ id: record.id,
385
+ status: "closed" as const,
386
+ agent: `worker-${record.id}`,
387
+ result: `result of ${record.id}`,
388
+ startedAt: 1000,
389
+ endedAt: 2000,
390
+ }),
391
+ listRecords: (limit) => storeRecords.slice(0, limit),
392
+ // flushBatch 签名已 async 化(deps 契约 Promise<void>);本 fake 只透传 notifyBatch
393
+ flushBatch: async (members) => {
394
+ notifier.notifyBatch(members);
395
+ },
396
+ });
397
+ return { mock, notifier, coordinator };
398
+ }
399
+
400
+ /** [U8 拆批修复] 闭合合批排程窗口等待:flush 排程 = setTimeout(0)(Node 1ms clamp),
401
+ * 10ms 真实定时器保证窗口已开(本包 vitest 真实 timers 环境)。 */
402
+ async function settleFlush(): Promise<void> {
403
+ await new Promise((resolve) => setTimeout(resolve, 10));
404
+ }
405
+
406
+ it("分两轮派 2+1 sync:闭合时单批 3 成员一次投递(D2)", async () => {
407
+ // 第一轮:2 sync 终态,第三台仍在跑(非终态 → 不闭合)
408
+ const storeRecords = [
409
+ makeStoreRec("sa-1", "sync", "closed"),
410
+ makeStoreRec("sa-2", "sync", "closed"),
411
+ makeStoreRec("sa-3", "sync", "running"),
412
+ ];
413
+ const { mock, coordinator } = makeHarness(storeRecords);
414
+
415
+ expect(coordinator.route(makeExecutionRecord("sa-1", "sync"))).toBe("sync-buffered");
416
+ expect(coordinator.route(makeExecutionRecord("sa-2", "sync"))).toBe("sync-buffered");
417
+ expect(coordinator.pendingCount).toBe(2);
418
+ expect(mock.sentMessages).toHaveLength(0);
419
+
420
+ // 第二轮:sa-3 终态(store 快照同步终态)→ 闭合 → 单批 3 成员
421
+ storeRecords[2] = makeStoreRec("sa-3", "sync", "closed");
422
+ expect(coordinator.route(makeExecutionRecord("sa-3", "sync"))).toBe("sync-flushed");
423
+ await settleFlush();
424
+ expect(coordinator.pendingCount).toBe(0);
425
+
426
+ expect(mock.sentMessages).toHaveLength(1);
427
+ const sent = mock.sentMessages[0]!;
428
+ expect(sent.content).toContain("Subagent batch completed: 3 finished, 0 failed, 0 cancelled.");
429
+ expect(sent.content).toContain("Subagent batch completed: 3 finished");
430
+ const details = sent.details as { batch: boolean; notifyId: string; items: BgNotifyRecord[] };
431
+ expect(details.items.map((i) => i.id).sort()).toEqual(["sa-1", "sa-2", "sa-3"]);
432
+ expect(details.notifyId).toBe(buildBatchNotifyId(["sa-1", "sa-2", "sa-3"]));
433
+ });
434
+
435
+ it("flush 后新 sync 成员开新批(缓冲清空语义),两批 hash 互异", async () => {
436
+ const storeRecords = [makeStoreRec("sa-1", "sync", "closed")];
437
+ const { mock, coordinator } = makeHarness(storeRecords);
438
+
439
+ expect(coordinator.route(makeExecutionRecord("sa-1", "sync"))).toBe("sync-flushed");
440
+ await settleFlush();
441
+ // 第一批已投(仍挂 pending——idle),第二批成员入缓冲
442
+ storeRecords.push(makeStoreRec("sa-2", "sync", "closed"));
443
+ expect(coordinator.route(makeExecutionRecord("sa-2", "sync"))).toBe("sync-flushed");
444
+ await settleFlush();
445
+
446
+ expect(mock.sentMessages).toHaveLength(2);
447
+ const ids = mock.sentMessages.map((m) => (m.details as { notifyId: string }).notifyId);
448
+ expect(new Set(ids).size).toBe(2);
449
+ expect(ids).toContain(buildBatchNotifyId(["sa-1"]));
450
+ expect(ids).toContain(buildBatchNotifyId(["sa-2"]));
451
+ });
452
+
453
+ it("混派正交(A8):async record 直通不走批,sync 成员批闭合不受 async 干扰", async () => {
454
+ const storeRecords = [
455
+ makeStoreRec("sa-sync", "sync", "closed"),
456
+ makeStoreRec("sa-async", undefined, "closed"),
457
+ ];
458
+ const asyncDirect: BgNotifyRecord[] = [];
459
+ const mock = makeLedgerHost();
460
+ bindNotifyLedgerHost(mock.host);
461
+ const notifier = createNotifier({
462
+ sendMessage: () => {},
463
+ hasRunningBackground: () => false,
464
+ isIdle: () => true,
465
+ });
466
+ const coordinator = new CollectCoordinator({
467
+ notifyAsync: (record) => {
468
+ asyncDirect.push({
469
+ id: record.id,
470
+ status: "closed",
471
+ agent: `worker-${record.id}`,
472
+ result: `result of ${record.id}`,
473
+ startedAt: 1000,
474
+ endedAt: 2000,
475
+ });
476
+ },
477
+ toNotifyRecord: (record) => ({
478
+ id: record.id,
479
+ status: "closed" as const,
480
+ agent: `worker-${record.id}`,
481
+ result: `result of ${record.id}`,
482
+ startedAt: 1000,
483
+ endedAt: 2000,
484
+ }),
485
+ listRecords: (limit) => storeRecords.slice(0, limit),
486
+ flushBatch: async (members) => {
487
+ notifier.notifyBatch(members);
488
+ },
489
+ });
490
+
491
+ expect(coordinator.route(makeExecutionRecord("sa-async", undefined))).toBe("async");
492
+ expect(coordinator.route(makeExecutionRecord("sa-sync", "sync"))).toBe("sync-flushed");
493
+ await settleFlush();
494
+ // async 直通收集器(生产接 notifier.notify),批通道只见 sync 成员
495
+ expect(asyncDirect.map((r) => r.id)).toEqual(["sa-async"]);
496
+ expect(mock.sentMessages).toHaveLength(1);
497
+ const details = mock.sentMessages[0]!.details as { items: BgNotifyRecord[] };
498
+ expect(details.items.map((i) => i.id)).toEqual(["sa-sync"]);
499
+ });
500
+ });
@@ -1186,6 +1186,33 @@ describe("NotifyLedger — T4③ 重投止损(PS-6)", () => {
1186
1186
  ledger.dispose();
1187
1187
  });
1188
1188
 
1189
+ it("同 notifyId 在账重复 record 幂等拒绝零副作用(⛔2 补强:U3 批 hash 幂等依赖)", () => {
1190
+ const mock = makeLedgerHost();
1191
+ const ledger = createNotifyLedger(mock.host);
1192
+
1193
+ expect(ledger.record("sa-dup", "content-v1", { notifyId: "sa-dup", agent: "first" })).toBe(true);
1194
+ const ledgerEntriesBefore = mock.entries.filter((e) => e.customType === NOTIFY_LEDGER_CUSTOM_TYPE).length;
1195
+ expect(ledgerEntriesBefore).toBe(1);
1196
+
1197
+ // 同 notifyId 二次 record:返回 false,零副作用(不新增 entry、账面不被覆盖)
1198
+ expect(ledger.record("sa-dup", "content-v2-attempted-overwrite", { notifyId: "sa-dup", agent: "second" })).toBe(false);
1199
+ expect(mock.entries.filter((e) => e.customType === NOTIFY_LEDGER_CUSTOM_TYPE)).toHaveLength(ledgerEntriesBefore);
1200
+ expect(ledger.pendingCount()).toBe(1);
1201
+
1202
+ // 投递面零副作用:只送首条 content(后写不覆盖),回执匹配不受污染
1203
+ mock.setIdle(true);
1204
+ ledger.attemptDeliver();
1205
+ expect(mock.sentMessages).toHaveLength(1);
1206
+ expect(mock.sentMessages[0]?.content).toBe("content-v1");
1207
+
1208
+ // 销账后同号仍拒绝(已销账幂等,E1 补发窗口依赖)
1209
+ fireSettled(mock);
1210
+ expect(ledger.waitingReceiptCount()).toBe(0);
1211
+ expect(ledger.record("sa-dup", "content-v1", { notifyId: "sa-dup" })).toBe(false);
1212
+
1213
+ ledger.dispose();
1214
+ });
1215
+
1189
1216
  it("③ 未达上限时正常重投行为不变:照常超时重投,回执到达后照常销账", () => {
1190
1217
  const mock = makeLedgerHost();
1191
1218
  // 前三轮回执不可匹配(重投路径)