@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
@@ -1,6 +1,6 @@
1
1
  // 执行编排 + 记录领域 Service(D4 按变化轴拆分后的编排核:execute/executeAndAwait 入口、
2
- // record 生命周期、cancel)。通知簇 → notify-host.ts;轮次结算闭包 → round-settlement.ts;
3
- // 冷路径复活 → cold-resurrect.ts。
2
+ // record 生命周期、cancel)。通知簇 → notify-host.ts;轮次结算闭包内联于
3
+ // settleChatRoundFromResponse(原 round-settlement.ts 已随 W3 inproc 删除收敛于此);冷路径复活 → cold-resurrect.ts。
4
4
  // 上游:subagent-tool(execute/query/cancel)、TUI(onChange/collectRecords)。
5
5
  // session_start 时经 initSession 注入 pi;modelRegistry/entries 归 ModelConfigService.initModel。
6
6
 
@@ -16,11 +16,14 @@ import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
16
16
  import { mapToWorkflowAgentResult } from "./agent-result-mapper.ts";
17
17
  import { removeAliveMarker } from "./alive-store.ts";
18
18
  import { bestEffort } from "./best-effort.ts";
19
- // [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程)。
19
+ import { CollectCoordinator } from "./collect-coordinator.ts";
20
+ import { DEFAULT_COLLECT_SYNC } from "./config.ts";
21
+ // [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程);
22
+ // idle 相位帧(协议 host/roundLifecycle)= arm 锚点(W3 v1.x——引擎不实现 idle 定时器)。
20
23
  // [T4②] DEFAULT_IDLE_TIMEOUT_MS:assertIdleTimeoutMsSafe 错误文案的缺省时长基准。
21
24
  // ([M3] hasIdleTimer / hasLiveProcessHandle 的 piAdapter.hasRunningBackground 过滤随通知簇
22
25
  // 在 notify-host.ts 消费,不经本文件。)
23
- import { disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
26
+ import { armIdleTimer, disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
24
27
  import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
25
28
  import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
26
29
  import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "./cold-resurrect.ts";
@@ -36,44 +39,72 @@ import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
36
39
  import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
37
40
  import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
38
41
  import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
39
- import { PiEngine } from "./engine/engines/pi/pi-engine.ts";
40
- import { PI_POOL_KEY } from "./engine/engines/pi/pi-engine.ts";
41
- import type { ChatRoundTicket, PiEngineService } from "./engine/engines/pi/pi-engine.ts";
42
+ import type { AgentCallOpts } from "../orchestration/models/types.ts";
43
+ import { setHostUiRequestEndpoint } from "./engine/host/host-ui-endpoint.ts";
44
+ // [W3 chat 域收口] chat 轮次与 run 域同路:经 pi-host-binding 解析 registry 'pi' 的
45
+ // cli 形态 port(RemoteEngine),协议 run(interact) 发往 pi-subagent-cli 引擎进程。
46
+ // inproc PiEngine(inproc pi 引擎目录)已删——本文件不再持有任何本地引擎实例。
47
+ import { PI_POOL_KEY, resolveHostPiEnginePort } from "./engine/host/pi-host-binding.ts";
48
+ import type { HostBridgeServiceFace } from "./engine/host/host-bridge.ts";
42
49
  import type { EnginePort, RunContext } from "./engine/port.ts";
43
50
  import { DEFAULT_ENGINE_ID, getEngine, listEngines } from "./engine/registry.ts";
44
- import { validateModelForEngine, withCrossEngineHint } from "./engine/model-validation.ts";
51
+ import {
52
+ joinEngineModelRef,
53
+ splitEngineModelRef,
54
+ validateModelForEngine,
55
+ withCrossEngineHint,
56
+ } from "./engine/model-validation.ts";
45
57
  import { type EngineRouteResult, routeEngineForHost } from "./engine/routing.ts";
46
- import type { AgentOutcome } from "./engine/types.ts";
58
+ import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
47
59
  import { ManifestStore } from "./manifest-store.ts";
48
60
  import type { ModelConfigService } from "./model-config-service.ts";
49
61
  import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
50
62
  import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts";
51
63
  // [T4④ / PS-5] flush 被门拦时的未投递 pending 落盘账本(persistUndeliveredNotificationsForReplay 消费)
64
+ import type { BatchBudgetParams, BgNotifyRecord } from "./notifier.ts";
52
65
  import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
53
66
  import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
54
- import { createRoundSettler } from "./round-settlement.ts";
55
67
  import type { StatusFilter } from "./record-store.ts";
56
68
  import { RecordStore } from "./record-store.ts";
69
+ // [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
70
+ //(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
71
+ import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
57
72
  import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
73
+ // [W6 宿主面下沉 → W3 纯镜像] killAll / killRecord / register 三函数经
74
+ // spawnedChildren 状态镜像公共面(engine/host/spawned-children.ts)——子进程活在
75
+ // 引擎进程内,本模块只做镜像记账(终止意图位),实际终止经协议 interact cancel/close。
58
76
  import {
59
77
  killAllSpawnedChildren,
60
78
  killRecordChildWithEscalation,
61
79
  registerSpawnedChildForRecord,
62
- runSpawn,
63
- type SessionRunnerContext,
64
- type SpawnResumeOpts,
65
- } from "./engine/engines/pi/session-runner.ts";
66
- // [u-t2a T2②/T2③] settled watchdog:chatMode 轮 settled 等待固定硬上限(10min),
67
- // 双挂载点之一在编排层热路径(deliverChatMessage,prompt 发出后),disarm 站点散布
68
- // cancel/close/终态化路径(原语幂等,见 settled-watchdog.ts 头注释)。
80
+ } from "./engine/host/spawned-children.ts";
81
+ // [u-t2a T2②/T2③ + W4 协议事件面] settled watchdog:chatMode 轮 settled 等待两段守护。
82
+ // W3 起事件接线 = 协议事件流(arm 点 = 轮开始;refresh 源 = streamDelta/轮次生命周期
83
+ // 帧;W4 三入口与旧原语逐一同构,见 settled-watchdog.ts 尾段)。
69
84
  import {
70
85
  armMidRoundNoProgress,
86
+ disarmRoundFromProtocol,
71
87
  disarmSettledWatchdog,
88
+ noteRoundSettledFromProtocol,
89
+ refreshFromProtocolEvent,
72
90
  type SettledWatchdogFireInfo,
73
91
  } from "./settled-watchdog.ts";
74
92
  import { isIdle, isResumable } from "./lifecycle-predicates.ts";
75
93
  import { startIdleGc } from "./idle-gc.ts";
76
- import { resetAllEpipeFailures } from "./engine/engines/pi/stdin-writer.ts";
94
+ // [W4] 轮次活性监督器(D2「等待有主」权威层;机制与注释见 round-supervisor/,
95
+ // 装配绑定面在 service-binding.ts——变化轴独立)
96
+ import type { RoundSupervisor } from "./round-supervisor/index.ts";
97
+ import {
98
+ createRoundSupervisorForService,
99
+ runPendingReconcileSweepForService,
100
+ } from "./round-supervisor/service-binding.ts";
101
+ // [W4] WorkflowRun store 纳入 idle-gc(startedAt 锚归档,宿主无关实现,见 file-run-store.ts)
102
+ import { FileRunStore } from "../orchestration/file-run-store.ts";
103
+ // [F-1 修复] idle-gc 的 WorkflowRun store 读根 = pi 壳 JsonlRunStore 同源布局
104
+ import { resolvePiWorkflowStateDir } from "./workflow-state-root.ts";
105
+ // [W4] 引擎进程死亡分诊(表 3 行 1 判据:EngineSdkError engine_crashed)
106
+ import { EngineSdkError } from "@zhushanwen/subagent-engine-sdk";
107
+ import type { HostRoundLifecycleParams, ProtocolError, ResumeAnchor } from "@zhushanwen/subagent-engine-sdk";
77
108
  import type { StreamSink, SubagentStream } from "./stream-sink.ts";
78
109
  import { createBackgroundStream } from "./stream-sink.ts";
79
110
  import { writeCancelledTombstone } from "./tombstone-store.ts";
@@ -136,13 +167,13 @@ export interface SubagentChatActions {
136
167
  * 子进程彻底 close 前(pi 子进程 trap SIGTERM 做 graceful shutdown,窗口几十~几百 ms),
137
168
  * 子进程的 trailing extension_ui_request 仍可能被父进程 pump 解析,调到 A 的 handler 闭包。
138
169
  * 若 dispose 不清 uiRequestHandler,旧 handler 闭包仍持有 A 的 ctx,触发
139
- * ui-request-queue.ts 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
170
+ * inproc UI 请求队列(已删) 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
140
171
  * logger.error(看起来像 bug,实际是预期竞态;三层兜底已确保功能正确)。
141
172
  *
142
173
  * stub 始终返回 {cancelled:true},不调 ctx.ui、不捕获任何 ctx,让 trailing ui_request
143
174
  * 干净降级为 cancelled(等价于子进程主动取消)。
144
175
  *
145
- * 不置 undefined —— 那会让 trailing ui_request 走 ui-request-queue.ts 的 handler-missing
176
+ * 不置 undefined —— 那会让 trailing ui_request 走 inproc UI 请求队列(已删) 的 handler-missing
146
177
  * 分支触发 notifyMissingHandlerGlobal warn,噪声性质从 threw-error 变 missing-handler,
147
178
  * 没真正解决。 */
148
179
  const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
@@ -242,6 +273,11 @@ const ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
242
273
  const ENV_DEPTH = "PI_SUBAGENT_DEPTH";
243
274
  const ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
244
275
 
276
+ /** [v2 D4] E1 等待分支 settled 有界重扫上限。无上限重扫 = 泄漏(设计 §3.3 D4 被否
277
+ * 谱系);8 次覆盖重启后主 agent 对 resumable 成员的典型续跑轮次,达限仍有
278
+ * running → disposed,交下次 session_start 收敛。 */
279
+ const SETTLED_RESCAN_LIMIT = 8;
280
+
245
281
  /** resolveIdentity 的产物——一次确定、写入 record 后不再变。 */
246
282
  interface ResolvedIdentity {
247
283
  agent: string;
@@ -314,12 +350,30 @@ export class SubagentService {
314
350
  /** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
315
351
  * 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
316
352
  * deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
317
- * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。 */
353
+ * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
354
+ * [sync-collect 合并] toNotifyRecord/notify/notifyBatch 随批路由需要由 host 导出
355
+ * (collectCoordinator 闭包与 E9/E1 直发路径消费,见 notify-host.ts 接口注释)。 */
318
356
  private readonly notifyHost: NotifyHost = createNotifyHost({
319
357
  getPi: () => this.pi,
320
358
  listRunning: () => this.store.listRunning(),
321
359
  getIsIdle: () => this.isIdleFn,
322
360
  });
361
+
362
+ /** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
363
+ private readonly collectCoordinator: CollectCoordinator;
364
+ /** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
365
+ * 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
366
+ * 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
367
+ * sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
368
+ * 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
369
+ private readonly e9ConvertedIds = new Set<string>();
370
+ /** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
371
+ * 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
372
+ * 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
373
+ * pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
374
+ * 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
375
+ * 的文件域);dispose 的惰化处置见 dispose()。 */
376
+ private settledRescanState: { disposed: boolean; scans: number } | null = null;
323
377
  /** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
324
378
  * 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
325
379
  * parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
@@ -341,22 +395,15 @@ export class SubagentService {
341
395
  * 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
342
396
  private readonly resumesInFlight = new Set<string>();
343
397
 
344
- /** chat 域 pi 引擎实例(D2 单轨:chat 域执行/投递统一经 EnginePort)。per-service DI——
345
- * getService 经适配器绑本实例;registry 全局 'pi' 单例绑进程级 getSubagentService(),
346
- * 直构 Service 的测试场景解析不到本实例。不能 import registration.ts(其 import 本文件
347
- * 循环依赖),直接构造 PiEnginepi-engine 不反向依赖本文件)。 */
348
- private readonly chatPiEngine: PiEngine = new PiEngine({ getService: () => this.piEngineServiceAdapter() });
349
-
350
- /** chat 域轮次交接包(executeViaEngine / 冷路径续轮挂载 → PiEngine.run 经 taskId 消费)。 */
351
- private readonly chatRoundTickets = new Map<string, ChatRoundTicket>();
352
-
353
- /** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
354
- * 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
355
- private readonly settleRound = createRoundSettler({
356
- notifyComplete: (record) => this.notifyHost.notifyComplete(record),
357
- reportRecordTransition: (record) => this.store.reportRecordTransition(record),
358
- closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record),
359
- });
398
+ /**
399
+ * [W3 chat 域收口] chat 轮次反向通道路由表(recordId 键):首轮 run chat 注册、
400
+ * interact 续聊轮复用——host/streamDelta(recordId 分支)与 host/roundLifecycle
401
+ * (recordId 键)的分发目标(EngineClient.recordRoutes
402
+ * EnginePort.registerChatRoundRoute 注册)。record 终态化路径注销([F-5 修复]
403
+ * 汇聚点 = doFinalizeRecord 的 onFinalized 钩子 + cancelBackground /
404
+ * disposeAllRecords 两处直连终态化路径显式注销)。
405
+ */
406
+ private readonly chatRoundRoutes = new Map<string, () => void>();
360
407
 
361
408
  /** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
362
409
  * deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
@@ -370,6 +417,21 @@ export class SubagentService {
370
417
  getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
371
418
  };
372
419
 
420
+ /**
421
+ * [W4] 轮次活性监督器(D2「等待有主」权威层):死亡事件纳管(run failed / 引擎
422
+ * exited → adoptOnProcessDeath)+ boot 分区重认领(initSession)+ 三态判定
423
+ * (record 级视图)+ 该放弃(终态化 failed + 注销 + 终止通知)。装配绑定面
424
+ * (deps/giveUp 编排/sweep 挂点)在 round-supervisor/service-binding.ts——变化轴
425
+ * 独立(通知文案 / 终态化编排 / store 读侧判据只动该文件),本字段只持实例。
426
+ */
427
+ private readonly roundSupervisor: RoundSupervisor = createRoundSupervisorForService({
428
+ getStore: () => this.store,
429
+ getPi: () => this.pi,
430
+ getSessionRootId: () => this.sessionRootId,
431
+ getMainSessionFile: () => this.mainSessionFile,
432
+ finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
433
+ });
434
+
373
435
  /** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
374
436
  * 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
375
437
  readonly queries: SubagentQueries = {
@@ -390,6 +452,12 @@ export class SubagentService {
390
452
 
391
453
  private readonly manifestStore: ManifestStore;
392
454
 
455
+ /**
456
+ * [D6 #7a] records 目录(与 manifestStore 同源同一推导)——屏障失败 warn 带 manifest
457
+ * 文件路径用(ManifestStore.dir 私有,此处不破封装另存同源值;漂移由构造点同语句保证不发生)。
458
+ */
459
+ private readonly recordsDir: string;
460
+
393
461
  /**
394
462
  * [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
395
463
  * 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
@@ -402,6 +470,9 @@ export class SubagentService {
402
470
  this.modelService = init.modelService;
403
471
  this.getMainSessionFile = init.getMainSessionFile;
404
472
  this.uiRequestHandler = init.uiRequestHandler;
473
+ // [W6 R3 MF-A] 壳侧应答端登记:cli 形态引擎经 host/askUser 反向请求消费(discovery
474
+ // portFactory 构造 EngineClient 时读取该登记)。
475
+ setHostUiRequestEndpoint(init.uiRequestHandler);
405
476
  this.pool = new DefaultConcurrencyPool(this.modelService.getGlobalConfig().maxConcurrent);
406
477
  this.worktreeManager = new WorktreeManager(this.modelService.getAgentDir());
407
478
  // [MF-3] worktree 隔离下全树落盘目录统一到 ROOT cwd:子进程(spawn cwd = worktree checkout 路径)
@@ -414,9 +485,84 @@ export class SubagentService {
414
485
  const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
415
486
  const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
416
487
  this.sessionsDir = sessionsDir;
488
+ this.recordsDir = recordsDir;
417
489
  this.manifestStore = new ManifestStore(recordsDir);
418
490
  this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? undefined);
419
- // #11:注册进程级 observability 单例——ui-request-queue.handleUiRequest
491
+ // collectCoordinator(subagent-sync-collect U2):notifyComplete 唯一路由入口——
492
+ // async 直通(字节不变)/ sync 批缓冲 + 闭合检测。[U3 接线点] 已接线:flush =
493
+ // manifest 屏障(await 全部落盘)→ notifier.notifyBatch 单条批投递(幂等键
494
+ // sync-batch:<hash> + 闭合触发排程合批 flush:同宏任务去抖窗口收纳背靠背
495
+ // route——U8 拆批盲窗修复 → ledger 写账 → attemptDeliver 边沿投递)+
496
+ // batchFinalized 落标(出口①,见下方闭包注释)。屏障先于写账 = 「通知可达 ⇒
497
+ // 索引就位」的构造性保证(时序竞态修复,见 flushBatch 闭包注释)。
498
+ // [U8] 排程与 E9/E1 交互:dispose 经 convertPendingSyncBufferToAsync 先取消挂起
499
+ // 排程(取消而非同步 flush——双通道并发写账防护,见该函数注释);E1 只在
500
+ // session_start 编排处运行(此前 dispose 已取消排程,补发直走 notifyBatch 不经
501
+ // 协调器),异常时序相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
502
+ // [D4-① 合并] notifier 实例封装在 notifyHost 内(createNotifyHost 装配),本类
503
+ // 经 this.notifyHost.notify / notifyBatch / toNotifyRecord 出口消费。
504
+ this.collectCoordinator = new CollectCoordinator({
505
+ notifyAsync: (record) => {
506
+ const notify = this.notifyHost.toNotifyRecord(record);
507
+ if (notify) this.notifyHost.notify(notify);
508
+ },
509
+ toNotifyRecord: (record) => this.notifyHost.toNotifyRecord(record),
510
+ // 闭合判定数据源:listAllActive(原始 ExecutionRecord 内存态,携带 collectMode/
511
+ // batchFinalized 原始值)。[U3 修正] 原接 collectRecords——其经 recordToSubagent
512
+ // 投影丢 collectMode(U2 披露的投影缺口)→ 真链上闭合判定恒立即闭合、跨轮续累
513
+ // 失效(真链 trace 实证)。非终态 sync 成员必在内存(archive 只删终态;磁盘重建
514
+ // 残留属孤儿恢复域),内存视图语义完整。
515
+ listRecords: (limit) => this.store.listAllActive().slice(0, limit),
516
+ flushBatch: async (members) => {
517
+ // [E9 防御过滤] 排除 dispose 时已转 async 写账的成员(同进程 revive 后协调器
518
+ // 缓冲残留的陈旧快照,见 e9ConvertedIds 字段注释);全被排除 → 空批零副作用
519
+ // 返回(成员已单独写账+落标,无需再动)。
520
+ const live = members.filter((m) => !this.e9ConvertedIds.has(m.id));
521
+ if (live.length === 0) return;
522
+ // 成员全量快照:getFullRecord 冷路径重建(成员在 notifyComplete 前已 archive,
523
+ // 内存无;闭合判定 hasRunningSync 的 listRecords 扫描已建 idToFile 索引,此处
524
+ // 命中)。快照在屏障前一次取定,屏障写与落标共用同一份(manifest 与落标 entry
525
+ // 字段同源)。
526
+ // [S11] getFullRecord 不可达(子 session 文件缺失/已 GC 的窗口)的成员改用
527
+ // 缓冲快照兜底落标,不再跳过——该成员仍随 live 进批写账(成员集 hash 含它),
528
+ // 跳过落标的旧行为会在重启后让 E1 重新收集它(无标记候选),以异成员集 hash
529
+ // 重新补发 → 同成员双投递(异 hash 不触发账本 sync-batch 幂等;旧注释「该
530
+ // 窗口下 E1 本也收不到该成员」不成立:E1 走主 session 文件末条 entry 扫描,
531
+ // 与 getFullRecord 的 manifest/子文件通路相互独立,manifest 屏障 best-effort
532
+ // 写失败同样制造此窗口)。
533
+ const fulls: SubagentRecord[] = [];
534
+ const fullMissed: BgNotifyRecord[] = [];
535
+ for (const m of live) {
536
+ const full = this.store.getFullRecord(m.id);
537
+ if (full) fulls.push(full);
538
+ else fullMissed.push(m);
539
+ }
540
+ // [时序屏障] 成员 manifest 写先于写账并 await 全部落盘——「通知可达 ⇒ 索引
541
+ // 就位」的构造性保证(by construction):批通知的指针行消费依赖
542
+ // records/<sa-id>.json 反查索引,fire-and-forget 下「通知送达时已落盘」只是
543
+ // 大概率成立(探针实测 mtime 相对 notify entry ±2/3ms 方向不定)。写失败仍
544
+ // best-effort(debug 不阻断写账投递,语义与 doFinalizeRecord Step 4 一致)。
545
+ await this.writeSyncBatchManifestBarrier(fulls);
546
+ // 单条批投递:accepted=false(同成员集批已在账——E1 重建重发/重复 flush)或
547
+ // 空批/dispose → 零副作用返回,不落标(设计 §3.1.3 出口①绑「写账成功」)。
548
+ const accepted = this.notifyHost.notifyBatch(live, this.getCollectSyncBudget());
549
+ if (!accepted) return;
550
+ // batchFinalized 纯落标(设计 §3.1.3 两出口之一:批闭合 flush 写账成功后)。
551
+ // collectMode/batchFinalized 显式覆写在 appendBatchFinalizedEntry 内(防非
552
+ // entry 源重建丢标记);末条 entry 带标记 → E1 重建扫描(collectMode=sync 且
553
+ // 无标记才收)据此排除,防双重通知。源序「写账先于落标」不变(v1 幂等窗口
554
+ // 语义);manifest 已在屏障提前写,幂等窗口内崩溃时索引更早已就位。
555
+ for (const full of fulls) {
556
+ this.appendBatchFinalizedEntry(full);
557
+ }
558
+ // [S11] miss 成员兜底落标(缓冲快照 → 最小标记 entry,映射见 sync-rebuild.ts),
559
+ // 与 fulls 同出口①语义(写账成功后统一补标)。
560
+ for (const m of fullMissed) {
561
+ this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? undefined));
562
+ }
563
+ },
564
+ });
565
+ // #11:注册进程级 observability 单例——inproc UI 请求队列 handleUiRequest(已删) 经
420
566
  // globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
421
567
  // warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
422
568
  registerGlobalObservability(this.uiObservability);
@@ -443,6 +589,8 @@ export class SubagentService {
443
589
  if (init.uiRequestHandler !== undefined) {
444
590
  this.uiRequestHandler = init.uiRequestHandler ?? undefined;
445
591
  this.uiObservability.resetMissingHandlerWarnings();
592
+ // [W6 R3 MF-A] session 级覆盖同步进壳侧应答端登记(三态:null = 显式清空)。
593
+ setHostUiRequestEndpoint(this.uiRequestHandler);
446
594
  }
447
595
  // SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
448
596
  // 透传 undefined,session-runner onClose 跳过 L2 清理(仅清 L1,保留旧行为)。
@@ -459,11 +607,33 @@ export class SubagentService {
459
607
  this.initExecContextBaseline(envRoot, init.sessionId);
460
608
  // revive(dispose 的逆操作:/resume /fork /new 后复活)
461
609
  this._disposed = false;
610
+ // [v2 D4] settled 重扫状态随 revive 重置:新 session 的 E1 若再判「仍有 running」
611
+ // 可重新注册。旧 handler 闭包捕获旧 state:正常时序(session_shutdown →
612
+ // session_start)下已随 dispose() 惰化;未经 dispose 的时序残留仍会在 settled
613
+ // 边沿执行——其扫描 this.mainSessionFile 当前值(非注册时的旧文件),行为等价于
614
+ // 新 session 多注册一次扫描,由账本 sync-batch:<hash> 幂等 + batchFinalized 候选
615
+ // 过滤收敛,无跨 session 污染面。
616
+ this.settledRescanState = null;
462
617
  this.store.revive();
463
618
  this.notifyHost.revive();
464
619
  // 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
465
620
  // sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
466
621
  this.recoverOrphansIfRootProcess();
622
+ // [W4] boot 分区 + 注册对账 sweep(须在孤儿恢复之后——依赖关系见两方法注释:
623
+ // 孤儿恢复把「重启前在途」record 直断 closed、把 resumable 形态保留 running 落
624
+ // entry,监督器重认领消费后者;sweep 再对终态 record 补发注销落盘——表 3 行 2
625
+ // 「注销经对账 sweep 保证落盘」的编排点)。
626
+ this.roundSupervisor.bootPartition();
627
+ runPendingReconcileSweepForService(
628
+ {
629
+ getStore: () => this.store,
630
+ getPi: () => this.pi,
631
+ getSessionRootId: () => this.sessionRootId,
632
+ getMainSessionFile: () => this.mainSessionFile,
633
+ finalizeClosed: (record, result) => this.finalizeRecord(record, result, "closed", "gc"),
634
+ },
635
+ (process.env[ENV_SELF_RECORD_ID] ?? "") !== "",
636
+ );
467
637
  }
468
638
 
469
639
  /**
@@ -531,11 +701,13 @@ export class SubagentService {
531
701
  /** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
532
702
  * private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
533
703
  * 是 initSession,转 private)。判定语义见 store 侧注释。
704
+ * mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
705
+ * 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
534
706
  * 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
535
707
  * E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。 */
536
708
  private recoverOrphanRecords(): void {
537
709
  try {
538
- this.store.recoverOrphanRecords(this.sessionRootId ?? undefined);
710
+ this.store.recoverOrphanRecords(this.sessionRootId ?? undefined, this.mainSessionFile);
539
711
  } catch (err) {
540
712
  logger.warn("[subagents] orphan recovery failed", {
541
713
  reason: toErrorMessage(err),
@@ -611,6 +783,9 @@ export class SubagentService {
611
783
  };
612
784
  completeRecord(record, result, "closed", reason);
613
785
  this.store.archive(record);
786
+ // [F-5 修复] 本路径不经 doFinalizeRecord(编排性关闭直连 completeRecord+archive),
787
+ // chat 轮路由注销在此补齐(幂等;闭包持 record/stream 引用,防泄漏)。
788
+ this.unregisterChatRoundRoute(record.id);
614
789
  // worktree 绑定清理(T3)。cleanup 已 async 化——同步签名(返回计数)不变,
615
790
  // 清理 fire-and-forget:失败经 bestEffort 留痕,不阻塞/不影响计数返回。
616
791
  if (record.worktreeHandle) {
@@ -641,10 +816,16 @@ export class SubagentService {
641
816
  /** SP-4: idle record GC(30 天 TTL,实现抽至 idle-gc.ts)。stop 函数(dispose 调)。 */
642
817
  private stopIdleGc: (() => void) | undefined;
643
818
 
644
- /** 启动 idle record GC 定时器(session_start 调用,幂等)。 */
819
+ /** 启动 idle record GC 定时器(session_start 调用,幂等)。
820
+ * [W4] WorkflowRun store(FileRunStore)同批纳入:running 且 startedAt 超 30 天
821
+ * 锚窗的 run 终态化归档(只终态化不补注销,见 idle-gc.ts 头注)。宿主未
822
+ * configureCore 时 loadAll 抛错由 idle-gc 内部吞掉(单轮跳过)。
823
+ * [F-1 修复] stateDir 与 pi 壳 JsonlRunStore 落盘布局同源
824
+ * (resolvePiWorkflowStateDir → <sessionDir>/workflow-state/)——缺省 dataRoot 根
825
+ * 与 pi 生产落盘不相交,WorkflowRun GC 曾恒空转(W4 引入的装配错位)。 */
645
826
  startGcTimer(): void {
646
827
  if (this.stopIdleGc) return;
647
- this.stopIdleGc = startIdleGc(this.store);
828
+ this.stopIdleGc = startIdleGc(this.store, new FileRunStore({ stateDir: resolvePiWorkflowStateDir() }));
648
829
  }
649
830
 
650
831
  /** 停止 idle record GC 定时器(dispose 调用)。 */
@@ -659,10 +840,271 @@ export class SubagentService {
659
840
  * listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
660
841
  * 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
661
842
  * 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
843
+ /** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
844
+ * 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
845
+ * → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
846
+ * sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
847
+ private getCollectSyncBudget(): BatchBudgetParams | undefined {
848
+ const cs = this.collectSyncSection();
849
+ return cs !== undefined ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : undefined;
850
+ }
851
+
852
+ /** [E9 专用] batchFinalized 落标 + manifest fire-and-forget 写(设计 §3.1.5 E9)。
853
+ * 批通知路径(flush/E1)已改走「manifest 屏障 → 写账 → 纯落标」序列(「通知可达
854
+ * ⇒ 索引就位」的构造性保证,见 flushBatch 闭包 / runSyncCollectRecoveryScan),
855
+ * 不再经本 helper;仅 E9 转换的成员保持原形态——其走 async 单条通知(全文注入、
856
+ * 无指针行消费),manifest 无时序要求,落标后 fire-and-forget 补写(list 后手动
857
+ * 反查的顺带索引)。
858
+ * 路径 = getFullRecord 冷路径重建 → appendBatchFinalizedEntry 纯落标 → fire
859
+ * manifest。getFullRecord 不可达(子 session 文件缺失/已 GC)→ 跳过该成员
860
+ * (详见 flushBatch 闭包注释)。 */
861
+ private markMembersBatchFinalized(memberIds: readonly string[]): void {
862
+ for (const id of memberIds) {
863
+ const full = this.store.getFullRecord(id);
864
+ if (!full) continue;
865
+ this.appendBatchFinalizedEntry(full);
866
+ // 反查索引缺失只影响指针行反查(session-reader 错误文案已指引绝对路径兜底),
867
+ // 不构成落标失败(与屏障路径的 best-effort 语义同源,仅无时序保证)。
868
+ void this.writeBatchMemberManifest(full).catch((err: unknown) => {
869
+ logger.debug(
870
+ `[subagents] batch-finalized manifest write failed (record=${full.id})`,
871
+ { reason: err instanceof Error ? err.message : String(err) },
872
+ );
873
+ });
874
+ }
875
+ }
876
+
877
+ /** batchFinalized 落标唯一出口(appendEntry 公共末步,纯落标):显式覆写
878
+ * collectMode/batchFinalized → reportSubagentRecord。覆写动机:recordToSubagent
879
+ * 投影已含两字段(U5 修复),但 getFullRecord 冷路径含 sidecar/manifest 重建分支
880
+ * (非 entry 源),显式赋值防非 entry 源重建时丢标记。
881
+ *
882
+ * [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
883
+ * 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
884
+ * records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
885
+ * 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
886
+ * await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
887
+ * fire-and-forget(async 单条通知无指针行消费,无时序要求)。
888
+ * rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
889
+ * 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
890
+ * task/slug/parentRecordId 等可选 undefined 自然缺省。 */
891
+ private appendBatchFinalizedEntry(rec: SubagentRecord): void {
892
+ this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
893
+ }
894
+
895
+ /** 批成员 manifest(sa- id → sessionFile 反查索引)写的唯一投影点(D2 字段投影 +
896
+ * status 如实投影:成功成员此刻 record 实态 running+resumable → "running",后续
897
+ * message upgrade 走完整 finalize 时 Step 4 原子覆盖为 "closed")。
898
+ * 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
899
+ * writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
900
+ * fire-and-forget catch(debug 不阻断落标)。 */
901
+ private writeBatchMemberManifest(rec: SubagentRecord): Promise<void> {
902
+ return this.manifestStore.writeManifest({
903
+ id: rec.id,
904
+ rootSessionId: rec.rootSessionId ?? "",
905
+ parentRecordId: rec.parentRecordId,
906
+ agentName: rec.agent,
907
+ status: rec.status,
908
+ createdAt: rec.startedAt,
909
+ completedAt: rec.endedAt,
910
+ sessionFile: rec.sessionFile,
911
+ task: rec.task,
912
+ slug: rec.slug,
913
+ model: rec.model,
914
+ });
915
+ }
916
+
917
+ /** [时序屏障] 批通知路径(flush/E1)专用:成员 manifest 并行写 + await 全部完成
918
+ * (allSettled)后才允许写账投递——「通知可达 ⇒ 索引就位」的构造性保证。写失败
919
+ * 不阻断(best-effort 语义与 doFinalizeRecord Step 4 一致:反查索引缺失只影响指针行
920
+ * 反查,session-reader 错误文案已指引绝对路径兜底,不构成写账失败);warn 留痕
921
+ * (D6 #7a / SC-1:屏障失败意味着该成员指针行反查索引缺失,debug 级在排障时不可见)。 */
922
+ private async writeSyncBatchManifestBarrier(recs: readonly SubagentRecord[]): Promise<void> {
923
+ const results = await Promise.allSettled(recs.map((rec) => this.writeBatchMemberManifest(rec)));
924
+ for (let i = 0; i < results.length; i++) {
925
+ const result = results[i]!;
926
+ if (result.status === "rejected") {
927
+ logger.warn(
928
+ `[subagents] batch-finalized manifest write failed (record=${recs[i]!.id}, manifest=${this.recordsDir}/${recs[i]!.id}.json)`,
929
+ { reason: result.reason instanceof Error ? result.reason.message : String(result.reason) },
930
+ );
931
+ }
932
+ }
933
+ }
934
+
935
+ /** [E9] dispose 时批未闭合:缓冲中已终态未通知成员逐条转 async 语义写账(放弃攒批)
936
+ * + 落 batchFinalized 标记(E1 重建扫描据此排除,防双重通知),交由既有 shutdown
937
+ * flush / resume 重放兑底;仍在跑的成员走现有退出路径(disposeAllRecords 关闭,
938
+ * 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
939
+ * 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
940
+ * 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
941
+ private convertPendingSyncBufferToAsync(): void {
942
+ // [U8 拆批修复·E9 交互] 挂起的合批排程先取消——dispose 已选「放弃攒批转 async」
943
+ // 语义(最简语义 = 取消而非同步 flush):放任排程触发会让 flushBatch(批 hash
944
+ // 通道)与下方逐条 notify(async id 通道)双通道并发写账 → 同成员双投递,且触发
945
+ // 点可能落在 notifier/store dispose 之后。取消后缓冲原样保留,本函数既有单通路
946
+ // 完整接管(协调器侧幂等:无排程时 no-op)。
947
+ this.collectCoordinator.cancelScheduledFlush();
948
+ const members = this.collectCoordinator.pendingMembers();
949
+ if (members.length === 0) return;
950
+ for (const member of members) {
951
+ this.e9ConvertedIds.add(member.id);
952
+ this.notifyHost.notify(member);
953
+ }
954
+ this.markMembersBatchFinalized(members.map((m) => m.id));
955
+ logger.warn(
956
+ `[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
957
+ { ids: members.map((m) => m.id) },
958
+ );
959
+ }
960
+
961
+ /**
962
+ * [E1] sync 批崩溃恢复钩子(设计 §3.1.5 E1,index.ts session_start 恢复编排处调用,
963
+ * 须晚于 initSession——孤儿终态恢复先行收敛 running 成员,「全员终态」判定才可达):
964
+ *
965
+ * - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
966
+ * collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
967
+ * 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
968
+ * - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
969
+ * 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
970
+ * - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
971
+ * 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
972
+ * entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
973
+ * 「已处理」);
974
+ * - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
975
+ * 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
976
+ * 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
977
+ * running 口径与协调器同构
978
+ * (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
979
+ * - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
980
+ * 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
981
+ * 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
982
+ *
983
+ * async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
984
+ * session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
985
+ * 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
986
+ *
987
+ * [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
988
+ * dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
989
+ * 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
990
+ */
991
+ async recoverSyncCollectBatch(): Promise<void> {
992
+ try {
993
+ const { outcome } = await this.runSyncCollectRecoveryScan();
994
+ // [v2 D4] 断链 4:等待分支不再死等——挂 settled 有界重扫(幂等单注册)。
995
+ if (outcome === "waiting") {
996
+ this.armSettledRescan();
997
+ }
998
+ } catch (err) {
999
+ // 与 index.ts 调用点原 try/catch 的 warn 容错同语义(该处 catch 对 async 化后
1000
+ // 的 promise 异常兑现不到,容错收敛到本方法内部)。
1001
+ logger.warn("[subagents] sync collect batch recovery failed", {
1002
+ reason: err instanceof Error ? err.message : String(err),
1003
+ });
1004
+ }
1005
+ }
1006
+
1007
+ /** [E1/D4] 单次「扫描→判定→可达则补发+落标」,E1 首扫与 settled 重扫共用同一实现
1008
+ * (防两处复制粘贴分岔)。三态返回:idle(无 sync 候选——已全部落标/E9 转换/异根,
1009
+ * 无事可等)/ waiting(仍有 running 成员,本次不动)/ dispatched(全员终态,已补发
1010
+ * +统一落标)。async:补发前有 manifest 屏障 await(见函数头 E1 注释)。 */
1011
+ /** 返回 outcome + waitingIds(达限 warn 需滞留成员 id,D6 #7b——仅 waiting 态非空)。 */
1012
+ private async runSyncCollectRecoveryScan(): Promise<{
1013
+ outcome: "idle" | "waiting" | "dispatched";
1014
+ waitingIds: string[];
1015
+ }> {
1016
+ const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
1017
+ if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
1018
+ const rootFilter = this.sessionRootId;
1019
+ const candidates = lastRecords.filter(
1020
+ (r) =>
1021
+ r.collectMode === "sync" &&
1022
+ r.batchFinalized !== true &&
1023
+ (rootFilter === undefined || r.rootSessionId === rootFilter),
1024
+ );
1025
+ if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
1026
+ // [v2 D3] 与协调器 hasRunningSync 同构口径(collect-coordinator.ts):running+
1027
+ // resumable 视为已完成、不阻止补发——成功成员崩溃时的末条 entry 恒为轮终
1028
+ // running+resumable(SP-5 有意语义),旧口径只看 status !== "closed" 会把主场景
1029
+ // (批内含成功成员)顶死在「等自然终态」永不补发(v2 §2.3 断链 3)。
1030
+ const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
1031
+ if (running.length > 0) {
1032
+ logger.debug(
1033
+ `[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
1034
+ { ids: running.map((r) => r.id) },
1035
+ );
1036
+ return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
1037
+ }
1038
+ // [时序屏障] manifest 先于写账 await 全部落盘——E1 补发同样是批通知(指针行消费
1039
+ // 依赖反查索引),「通知可达 ⇒ 索引就位」的构造性保证与 flushBatch 同款。
1040
+ await this.writeSyncBatchManifestBarrier(candidates);
1041
+ // 全员终态:单条批补发(budget 热读与 flushBatch 同源);账本同 hash 幂等拒绝也算
1042
+ // 已投递(批已在账/已销账,重放由账本承接)——两种结局统一补标。
1043
+ const members = candidates.map((r) => syncRebuildToNotifyMember(r));
1044
+ const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
1045
+ for (const rec of candidates) {
1046
+ this.appendBatchFinalizedEntry(rec);
1047
+ }
1048
+ logger.warn(
1049
+ `[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
1050
+ { ids: members.map((m) => m.id) },
1051
+ );
1052
+ return { outcome: "dispatched", waitingIds: [] };
1053
+ }
1054
+
1055
+ /** [v2 D4] 注册 agent_settled 有界重扫(幂等:settledRescanState 非 null 不叠加注册
1056
+ * ——E1 现仅 session_start 单调用点,守卫是防第二入口引入时的注册叠加断言面)。
1057
+ * 每次 settled 边沿重跑同一 E1 扫描(runSyncCollectRecoveryScan):dispatched
1058
+ * (补发+落标完成,scan 的 warn 已留痕)或 idle(候选已被其他通路落标)→ disposed;
1059
+ * 累计 SETTLED_RESCAN_LIMIT 次仍在等 → disposed + warn 留痕(D6 #7b / SC-2:达限
1060
+ * 放弃重扫意味着滞留成员的批通知要等下次 session_start 才收敛,含滞留 id 的 warn
1061
+ * 是唯一线索,debug 级排障不可见;后续事件零处理,下次 session_start 再收敛)。
1062
+ * pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
1063
+ * index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
1064
+ * pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
1065
+ * extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
1066
+ * extension 的全部 handler 逐一 await;故本注册与 ledger host 经
1067
+ * piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
1068
+ private armSettledRescan(): void {
1069
+ if (this.settledRescanState !== null) return;
1070
+ const state = { disposed: false, scans: 0 };
1071
+ this.settledRescanState = state;
1072
+ this.pi?.on?.("agent_settled", async () => {
1073
+ if (state.disposed) return;
1074
+ state.scans += 1;
1075
+ // await 完整补发序列(manifest 屏障 → 写账 → 落标):pi emit 对 handler 逐一
1076
+ // await(P-settled 定谳,0.84.4 dist runner.js),async 化不改变分发语义。
1077
+ const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
1078
+ if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
1079
+ state.disposed = true;
1080
+ if (outcome === "waiting") {
1081
+ // D6 #7b / SC-2:warn + 滞留成员 id(debug 级排障不可见——达限即批通知挂起至下次 session_start)
1082
+ logger.warn(
1083
+ `[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
1084
+ { ids: waitingIds },
1085
+ );
1086
+ }
1087
+ });
1088
+ }
1089
+
1090
+ // [E1 语义对齐 toNotifyRecord] 补发成员映射 syncRebuildToNotifyMember 拆至
1091
+ // sync-rebuild.ts(变化轴:恢复批通知语义):one-shot 成功成员末条恒
1092
+ // running+resumable(SP-5),直通 status 会让恢复批批头「0 finished」且丢
1093
+ // patchFile 的 git-apply 指针——对齐后补发记录为 closed + outcome 物化 +
1094
+ // patchFile 透传。调用点:runSyncCollectRecoveryScan。
1095
+
662
1096
  dispose(): void {
663
1097
  if (this._disposed) return;
664
1098
  this._disposed = true;
665
1099
  this.stopGcTimer();
1100
+ // [v2 D4] settled 重扫 handler 惰化:dispose 后 trailing settled 边沿若仍触发,
1101
+ // 旧 handler 不得再跑扫描——notifier 随后将 dispose,notifyBatch 短路返回 false
1102
+ // 且不写账,而 E1 dispatched 段不判 accepted 仍统一落标 → 批被标 batchFinalized
1103
+ // 而通知从未写账(永久丢失,不可逆)。惰化后通知由下次重启的 E1 首扫兑现
1104
+ // (成员无标记,候选可达)。与 initSession revive 重置不冲突:dispose 是终态置
1105
+ // disposed,revive 置 null 是新 session 的重新注册,旧 state 对象随旧 handler
1106
+ // 闭包保持 disposed 永久惰化。
1107
+ if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
666
1108
  // [dispose stub] 第一时间换 stub,防 trailing ui_request 调到 stale handler 闭包
667
1109
  // (仍持有 disposed session 的 ctx)产生误导性 console.error。stub 干净降级为 cancelled。
668
1110
  // 必须在 emit/abort 之前——这些步骤可能同步触发 trailing pump。
@@ -670,20 +1112,31 @@ export class SubagentService {
670
1112
  // 此处内联其方法体(赋值 + 缺失告警去重重置)。
671
1113
  this.uiRequestHandler = disposedUiRequestStub;
672
1114
  this.uiObservability.resetMissingHandlerWarnings();
1115
+ // [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
1116
+ // cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
1117
+ setHostUiRequestEndpoint(disposedUiRequestStub);
673
1118
  // [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
674
1119
  // abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
675
1120
  this.store.abortRunningControllers();
676
1121
  killAllSpawnedChildren();
1122
+ // [W3] chat 反向通道路由全量注销(inproc 轮次交接包清空的协议形态替位)。
1123
+ for (const recordId of [...this.chatRoundRoutes.keys()]) {
1124
+ this.unregisterChatRoundRoute(recordId);
1125
+ }
1126
+ // [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
1127
+ // 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
1128
+ // running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
1129
+ // 冷 idToFile(无目录扫描则 miss → 跳过落标 → E1 重建误收已转换成员);先转换取
1130
+ // 内存命中,与 flushBatch 出口①同款通路。同样在 flushPendingNotifications 之前
1131
+ //(转换条目加入本次 flush)与 notifier/store dispose 之前(写账与 appendEntry
1132
+ // 通道仍可用)。仍在跑成员不在此处理——后续 disposeAllRecords 按现有退出路径关闭。
1133
+ this.convertPendingSyncBufferToAsync();
677
1134
  // SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
678
1135
  // 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
679
1136
  this.disposeAllRecords("parent-shutdown");
680
- // [v4 A-1] EPIPE 连续失败计数器清零(计数器已迁移到 stdin-writer,防跨 session 泄漏)
681
- resetAllEpipeFailures();
682
1137
  // [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
683
1138
  // abort/kill 后仍挂着的条目,防跨 session 复活时残留)
684
1139
  this.resumesInFlight.clear();
685
- // chat 轮次交接包清空(正常由 PiEngine.run 消费;此处兜底 kill 后仍挂着的条目)
686
- this.chatRoundTickets.clear();
687
1140
  // flush 待发通知(session_shutdown 尽力投递一次)。
688
1141
  this.notifyHost.flushPendingNotifications();
689
1142
  // [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
@@ -693,6 +1146,9 @@ export class SubagentService {
693
1146
  // 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
694
1147
  this.persistUndeliveredNotificationsForReplay();
695
1148
  this.notifyHost.dispose();
1149
+ // [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
1150
+ // 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
1151
+ this.roundSupervisor.dispose();
696
1152
  this.store.dispose();
697
1153
  }
698
1154
 
@@ -791,7 +1247,9 @@ export class SubagentService {
791
1247
  // 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
792
1248
  // 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
793
1249
  // 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
794
- // record。pi 请求路径同步短路(routed 非 Promise,零微任务——缺省路径时序不变)。
1250
+ // record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
1251
+ // 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
1252
+ // 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
795
1253
  // [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
796
1254
  // engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
797
1255
  // 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
@@ -807,7 +1265,9 @@ export class SubagentService {
807
1265
  taskModel: opts.model,
808
1266
  strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
809
1267
  probe: (engineId) => getEngine(engineId).probe(),
810
- piEngine: this.chatPiEngine,
1268
+ // [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
1269
+ // 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
1270
+ piEngine: this.resolveChatEnginePort(),
811
1271
  });
812
1272
  const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
813
1273
  return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
@@ -867,42 +1327,74 @@ export class SubagentService {
867
1327
  // deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
868
1328
 
869
1329
  /**
870
- * [V2 决策 3] chatMode 统一投递入口(message action 的 Service 面)——经引擎交互面
871
- * 执行(D2:PiEngine.interactRecord——port face interact record 锚定形态,协议知识
872
- * 在引擎边界,编排层不做 stdin 写入)。分流语义(按**进程死活**,
873
- * 不按 record.status)与热/冷路径细节见 PiEngine.deliverPrompt:
1330
+ * [V2 决策 3 → W3 协议形态] chatMode 统一投递入口(message action 的 Service 面)
1331
+ * ——经协议 interact(message action)发往 pi-subagent-cli 引擎进程。协议知识
1332
+ * stdin prompt 命令 + streamingBehavior 映射 + EPIPE 兜底)在引擎进程内
1333
+ * (chat-session.deliverMessage);编排层按结构化结果分流:
874
1334
  *
875
- * 热路径(进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle(F3/F4)。
876
- * 冷路径(进程死):冷路径续轮(resumeChatRound)重开 session + prompt(仅崩溃/
877
- * timeout kill/跨重启命中)。
878
- *
879
- * 失败语义与直调形态一致:业务拒绝(not ready / EPIPE 兜底耗尽等,文案自带行动语言)
880
- * 经 interact 结构化结果回传后原样 throw(错误文本逐字节保持)。
1335
+ * 受理(引擎侧进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle
1336
+ * 冷路径(engine_session_not_resumable = 进程死/无活进程):冷路径续轮
1337
+ * (resumeColdRound → run chat + resume 锚点接续,仅崩溃/timeout kill/跨重启命中)。
1338
+ * 其他拒绝(EPIPE 兜底耗尽 / engine_interact_failed):业务拒绝原样 throw
1339
+ * (文案自带行动语言——与 inproc 形态逐字节一致)。
881
1340
  *
882
1341
  * @param record 目标 record(chatMode,running 或 idle)
883
1342
  * @param text 消息正文
884
- * @param interrupt true=steer(抢占)/ false=followUp(排队),仅热路径 streamingBehavior 用
1343
+ * @param interrupt true=steer(抢占)/ false=followUp(排队)
885
1344
  */
886
1345
  private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
887
1346
  this.assertReady();
888
- // interactRecord:interact record 锚定形态(调用方已持归属校验过的同一 record
889
- // 对象——port face interact 的 handle 解析在此冗余且会做二次 store 查找)
890
- const result = await this.chatPiEngine.interactRecord(
891
- record,
892
- { kind: "message", payload: text, interrupt },
893
- );
1347
+ // 终态守卫(MF-4):closed record 不可续聊(对齐原冷路径守卫前置——状态检查
1348
+ // 先于引擎交互,避免对已终态对话产生引擎侧副作用)。
1349
+ if (record.status !== "running") {
1350
+ throw new Error(
1351
+ `subagent ${record.id} is not ready for a new message (current state: ${record.status}). ` +
1352
+ `Recovery: use action:'list' to confirm state; wait for the current round to finish, or send the message again once it is idle.`,
1353
+ );
1354
+ }
1355
+ // 新 turn 开跑先 disarm idle timer(防 turn 期间误杀活进程——V2 决策 4,
1356
+ // 原 PiEngine.deliverPrompt 入口语义,编排侧承接)。
1357
+ disarmIdleTimer(record.id);
1358
+ const engine = this.resolveChatEnginePort();
1359
+ // recordId 键路由兜底注册(首轮已注册时幂等跳过——kickOffChatRound 注册面):
1360
+ // 续聊轮 delta/生命周期帧的分发目标。stream 缺省(首轮流已 dispose——widget 清除后
1361
+ // delta 为 no-op,与 inproc 形态一致),守护刷新与相位消费不受影响。
1362
+ if (!this.chatRoundRoutes.has(record.id)) {
1363
+ this.chatRoundRoutes.set(
1364
+ record.id,
1365
+ engine.registerChatRoundRoute?.(record.id, {
1366
+ onStreamDelta: (_delta) => refreshFromProtocolEvent(record.id),
1367
+ onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
1368
+ }) ?? (() => {}),
1369
+ );
1370
+ }
1371
+ const result = await engine.interact(this.chatHandleFor(record), {
1372
+ kind: "message",
1373
+ payload: text,
1374
+ interrupt,
1375
+ });
894
1376
  if (!result.ok) {
1377
+ // 冷路径分流:引擎无活进程 → 冷路径续轮(resume spawn 接续原 session——
1378
+ // W2 契约「引擎不自发 resume,宿主经 run chat+resume 接续」)。
1379
+ if (result.code === "engine_session_not_resumable") {
1380
+ this.resumeColdRound(record, text);
1381
+ return;
1382
+ }
895
1383
  throw new Error(result.message);
896
1384
  }
897
- // [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点,首轮
898
- // 调用点在 session-runner runSpawn)。原挂载位在 deliverMessage prompt 写入成功后
899
- // 的同步段——D2 协议知识下沉 PiEngine.deliverPrompt 后编排层在 interact 返回点
900
- // arm(deliverPrompt 热路径段无 await,返回点距 prompt 发出仅差微任务链;窗口口径
901
- // 「整轮含 turn 执行与收尾」不受影响)。冷路径(EPIPE 兜底/resume)下 runSpawn 的
902
- // 首轮 arm 与此处重复挂载由原语幂等(先清旧 timer)吸收 = 窗口重置,无害。
903
- // prompt 发出后挂**中段**无进展检测(有效协议事件行刷新,连续静默判 wedged)+
904
- // agent_end 交棒收尾段固定上界;settled 到达(PiEngine.handleSdkEvent disarm)/
905
- // 进程 close / cancel/close 终态化处置后即清(disarm 站点散布上述路径,幂等)。
1385
+ // 受理成功:轮始执行态信号清除 + 迁移上报(原 deliverPrompt 热路径同步段语义——
1386
+ // 清上一轮 result(§5.4 isStreaming 公式要求 result undefined 才显示 streaming)
1387
+ // resumable,appendEntry runtime 派生缓存失效、GUI 从 waiting 切回 spinner)。
1388
+ record.status = "running";
1389
+ record.result = undefined;
1390
+ record.resumable = undefined;
1391
+ this.store.reportRecordTransition(record);
1392
+ // [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点;首轮/
1393
+ // 冷续轮调用点在 kickOffChatRound run 派发前——[F-2 修复] 该点此前声称存在
1394
+ // 实则缺失,被删的 inproc stdout-pump 是首轮唯一中段守护)。prompt 发出后挂
1395
+ // **中段**无进展检测,刷新源 = 协议事件(recordId 键 streamDelta / roundLifecycle
1396
+ // 帧,见 chatRoundRoutes;首轮 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle);
1397
+ // settled 相位交棒(noteRoundSettledFromProtocol)、idle/close/cancel 终态化即清。
906
1398
  armMidRoundNoProgress(record.id, {
907
1399
  onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
908
1400
  onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
@@ -997,24 +1489,28 @@ export class SubagentService {
997
1489
  // W16 [D4]:冷路径续轮是类外状态写点(不走 register/archive),显式上报迁移。
998
1490
  this.store.reportRecordTransition(record);
999
1491
 
1000
- // resume 参数从 record identity 读(防漂移,P-10)。
1001
- const resume: SpawnResumeOpts = {
1002
- sessionFile: record.sessionFile,
1003
- model: record.model,
1004
- thinkingLevel: record.thinkingLevel,
1492
+ // [W3 协议形态] 冷续锚点 = record identity(防多轮对话模型漂移,探针 P-10):
1493
+ // sessionFile 经 resume.sessionRef 携带(引擎 --session 续写原文件);model/
1494
+ // thinkingLevel 属 record 身份字段(锚点轮引擎侧覆盖解析)。原 SpawnResumeOpts
1495
+ // 形态随 inproc pi 引擎目录 删除,锚点载体 = 协议 ResumeAnchor(W1 SDK 契约)。
1496
+ const resume: ResumeAnchor = {
1497
+ sessionRef: {
1498
+ recordId: record.id,
1499
+ ...(record.sessionFile !== undefined ? { sessionFile: record.sessionFile } : {}),
1500
+ },
1501
+ poolKey: PI_POOL_KEY,
1005
1502
  };
1006
1503
 
1007
- // 重建 resolved:runSpawn resume.model/thinkingLevel 优先(覆盖 resolved),
1008
- // resolved.model.id 仅在 runSpawn 内被读(resume 短路时不报错)。从 record.model
1009
- // (createRecordForMode 写入的 "provider/id" 格式)解析 provider/id 构造最小 ModelInfo。
1010
- const slashIdx = record.model.indexOf("/");
1011
- const provider = slashIdx >= 0 ? record.model.slice(0, slashIdx) : "unknown";
1012
- const modelId = slashIdx >= 0 ? record.model.slice(slashIdx + 1) : record.model;
1504
+ // 重建 resolved:锚点轮的模型解析兜底(ctxModel 第三层)。从 record.model
1505
+ // (createRecordForMode 写入的 "provider/id" 或无斜杠 ref——契约变更④)拆分构造
1506
+ // 最小 ModelInfo;拆分与写入侧同源(splitEngineModelRef):无斜杠 ref provider
1507
+ // 为空串(旧行为 "unknown" 会给续轮注入虚构 provider),整串进 name。
1508
+ const model = splitEngineModelRef(record.model);
1013
1509
  const identity: ResolvedIdentity = {
1014
1510
  agent: record.agent,
1015
1511
  agentConfig: undefined,
1016
1512
  resolved: {
1017
- model: { id: modelId, name: record.model, provider, reasoning: false },
1513
+ model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
1018
1514
  thinkingLevel: record.thinkingLevel,
1019
1515
  },
1020
1516
  };
@@ -1023,15 +1519,17 @@ export class SubagentService {
1023
1519
  task: text,
1024
1520
  slug: record.slug,
1025
1521
  worktree: record.worktreeHandle,
1522
+ // 冷续轮同样是 chat 会话形态(run.params.chat 必传 + conversation gate 放行)。
1523
+ conversation: true,
1026
1524
  };
1027
- const ctx = this.buildSessionRunnerContext();
1028
1525
 
1029
- // detached 编排:轮次在 background 跑,pool 重新 acquire(轮次间 idle release)。
1030
- // chatMode + done M2-A 分流自动 finalizeRoundToIdle(record 回 idle、round+1)。
1526
+ // detached 编排:轮次在 background 跑(协议 run chat + resume),pool 重新
1527
+ // acquire(轮次间 idle release)。chatMode 轮终 settle 分流自动
1528
+ // finalizeRoundToIdle(record 回 idle、round+1)。
1031
1529
  // [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
1032
- // (冷路径 / EPIPE 兜底)在守卫处被拒;轮次收尾 finally 统一清除。
1530
+ // (冷路径重入)在守卫处被拒;轮次收尾 finally 统一清除。
1033
1531
  this.resumesInFlight.add(record.id);
1034
- this.kickOffChatRound(record, opts, identity, ctx, record.controller.signal, PRIORITY_BACKGROUND, resume);
1532
+ this.kickOffChatRound(record, opts, identity, record.controller.signal, PRIORITY_BACKGROUND, resume);
1035
1533
  }
1036
1534
 
1037
1535
  /**
@@ -1059,6 +1557,9 @@ export class SubagentService {
1059
1557
  `terminating (LC-1 wedge recovery)`,
1060
1558
  );
1061
1559
  killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
1560
+ // [W3] 子进程在引擎进程内——终止经协议 interact cancel(SIGTERM → 引擎侧
1561
+ // settle 等待 → 杀链升级);镜像置死位由上方 killRecordChildWithEscalation 记账。
1562
+ this.terminateChatSession(record, "cancel", "settled watchdog (hot path)");
1062
1563
  const failedResult: AgentResult = {
1063
1564
  text: "",
1064
1565
  turns: record.turnCount,
@@ -1081,12 +1582,12 @@ export class SubagentService {
1081
1582
  // [D4-①] 通知簇搬移 notify-host 后的遗留调用点修正:this.notifyComplete 方法已
1082
1583
  // 不存在(其余三处调用点均 this.notifyHost.notifyComplete),旧引用 throw
1083
1584
  // TypeError 被本 .catch 吞掉 → settled watchdog 失败通知静默丢失
1084
- .then(() => this.notifyHost.notifyComplete(record))
1585
+ .then(() => this.collectCoordinator.route(record))
1085
1586
  .catch((err: unknown) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
1086
1587
  }
1087
1588
 
1088
1589
  // [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
1089
- // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/engines/pi/pi-engine.ts
1590
+ // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/inproc pi-engine(已删)
1090
1591
  // rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
1091
1592
  // 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
1092
1593
 
@@ -1210,13 +1711,16 @@ export class SubagentService {
1210
1711
  */
1211
1712
  private async closeChatIdle(record: ExecutionRecord): Promise<void> {
1212
1713
  // [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
1213
- // [T2④ / LC-2] SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
1214
- // 30s 升级 SIGKILL——终态化后 record 已 archive,此前裸 SIGTERM 挂住 = 幽灵进程
1215
- // 无任何后续回收通道。同时 disarm settled watchdog(本路径终态化 = settled 等待
1216
- // 窗口终结,防 watchdog 误杀后续同 id 资源)。
1714
+ // [T2④ / LC-2] 终止语义收敛:SIGTERM 被无视时 30s 升级 SIGKILL——终态化后
1715
+ // record 已 archive,挂住 = 幽灵进程无任何后续回收通道。同时 disarm settled
1716
+ // watchdog(本路径终态化 = settled 等待窗口终结,防 watchdog 误杀后续同 id 资源)。
1717
+ // [W3] 实际终止在引擎进程内(协议 interact close force → 引擎侧杀链);
1718
+ // killRecordChildWithEscalation 只做镜像置死记账。
1217
1719
  disarmIdleTimer(record.id);
1218
1720
  disarmSettledWatchdog(record.id);
1721
+ disarmRoundFromProtocol(record.id);
1219
1722
  killRecordChildWithEscalation(record.id, "closeChatIdle");
1723
+ this.terminateChatSession(record, "close", "closeChatIdle");
1220
1724
  // 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
1221
1725
  // `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
1222
1726
  // completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
@@ -1238,6 +1742,8 @@ export class SubagentService {
1238
1742
  modelService: this.modelService,
1239
1743
  pi: this.pi,
1240
1744
  emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
1745
+ // [F-5 修复] 同 finalizeRecord——chat 轮路由注销单一汇聚点钩子。
1746
+ onFinalized: (id) => this.unregisterChatRoundRoute(id),
1241
1747
  sessionDir: this.sessionsDir,
1242
1748
  },
1243
1749
  record,
@@ -1269,11 +1775,14 @@ export class SubagentService {
1269
1775
  */
1270
1776
  private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
1271
1777
  // 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
1272
- // [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation(30s 升级 SIGKILL
1273
- // 终态化后无后续回收通道);settled 等待窗口同步终结(disarm 幂等)。
1778
+ // [T2④ / LC-2] 终止语义收敛(30s 升级 SIGKILL,终态化后无后续回收通道);
1779
+ // settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
1780
+ // force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
1274
1781
  disarmIdleTimer(record.id);
1275
1782
  disarmSettledWatchdog(record.id);
1783
+ disarmRoundFromProtocol(record.id);
1276
1784
  killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
1785
+ this.terminateChatSession(record, "close", "closeAfterRoundSettled");
1277
1786
  if (!tryTransition(record, "closed", "user-close")) {
1278
1787
  return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
1279
1788
  }
@@ -1364,17 +1873,13 @@ export class SubagentService {
1364
1873
  }
1365
1874
  }
1366
1875
 
1367
- // ── 步骤 3: SessionRunnerContext ──
1368
- const ctx = this.buildSessionRunnerContext(opts.cwd);
1369
-
1370
- // ── 步骤 4: signal 决议 ──
1876
+ // ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
1371
1877
  const effectiveSignal = signal ?? record.controller?.signal;
1372
1878
 
1373
1879
  // 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
1374
1880
  const result = await this.runAndFinalize(
1375
1881
  record,
1376
1882
  { ...opts, worktree: worktreeHandle },
1377
- ctx,
1378
1883
  identity,
1379
1884
  effectiveSignal,
1380
1885
  PRIORITY_BACKGROUND,
@@ -1427,6 +1932,23 @@ export class SubagentService {
1427
1932
  return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? undefined);
1428
1933
  }
1429
1934
 
1935
+ /**
1936
+ * collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
1937
+ * startHandler 缺省 collect 解析用)。
1938
+ * config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
1939
+ * 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
1940
+ * reloadGlobalConfig 刷新)。
1941
+ */
1942
+ /** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
1943
+ * budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
1944
+ private collectSyncSection() {
1945
+ return this.modelService.getGlobalConfig().collectSync;
1946
+ }
1947
+
1948
+ getCollectSyncDefault(): "async" | "sync" {
1949
+ return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
1950
+ }
1951
+
1430
1952
  /** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
1431
1953
  * 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
1432
1954
  * 返回 undefined:id 不存在于内存与磁盘。 */
@@ -1506,19 +2028,21 @@ export class SubagentService {
1506
2028
  opts: ExecuteOptions,
1507
2029
  ): ResolvedIdentity {
1508
2030
  const canonical = validateModelForEngine(engine, engineModel);
1509
- // record.model 留痕:canonical(引擎裁决全名,含短名→缺省 provider 归一化);
1510
- // 引擎未实现校验面且无显式 model 时为空串(记录形态退化,生产不可达——注册表
1511
- // 内非 pi 引擎均实现 validateModel;防御性拼接避免 throw 打断兜底语义)。
2031
+ // record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
2032
+ // 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
2033
+ // 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
2034
+ // 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
2035
+ // 整串进 name,不再落 "<ref>/" 畸形)。
1512
2036
  const modelStr = canonical ?? engineModel ?? "";
1513
- const slashIdx = modelStr.indexOf("/");
2037
+ const model = splitEngineModelRef(modelStr);
1514
2038
  return {
1515
2039
  agent,
1516
2040
  agentConfig,
1517
2041
  resolved: {
1518
2042
  model: {
1519
- id: slashIdx > 0 ? modelStr.slice(slashIdx + 1) : "",
1520
- name: modelStr,
1521
- provider: slashIdx > 0 ? modelStr.slice(0, slashIdx) : modelStr,
2043
+ id: model.id,
2044
+ name: model.name,
2045
+ provider: model.provider,
1522
2046
  reasoning: false,
1523
2047
  },
1524
2048
  thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
@@ -1548,7 +2072,10 @@ export class SubagentService {
1548
2072
 
1549
2073
  const record = createRecord(id, {
1550
2074
  agent: identity.agent,
1551
- model: `${identity.resolved.model.provider}/${identity.resolved.model.id}`,
2075
+ // model 留痕词形与拆分同源(joinEngineModelRef):provider 为空串只写 id——
2076
+ // 契约变更④的无斜杠 ref(provider=""/id=ref)不得落成 "/ref" 或 "ref/" 畸形;
2077
+ // 续聊回读侧 splitEngineModelRef 对无斜杠串还原 provider=""/id=ref,往返自洽。
2078
+ model: joinEngineModelRef(identity.resolved.model),
1552
2079
  thinkingLevel: identity.resolved.thinkingLevel,
1553
2080
  mode,
1554
2081
  task: opts.task,
@@ -1563,6 +2090,10 @@ export class SubagentService {
1563
2090
  // 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
1564
2091
  engine: opts.engine,
1565
2092
  engineFallback: opts.engineFallback,
2093
+ // subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
2094
+ // 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
2095
+ // undefined = async(缺省语义,旧记录零迁移)。
2096
+ collectMode: opts.collect === "sync" ? "sync" : undefined,
1566
2097
  controller,
1567
2098
  });
1568
2099
 
@@ -1598,6 +2129,12 @@ export class SubagentService {
1598
2129
  // 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
1599
2130
  // 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
1600
2131
  // 检查若只落在 engine.run 内则拒绝异步化为「派发成功 + 静默失败 record」)。
2132
+ // [W3 契约变更③补注(协议化能力位方向判定)] 同步拒只覆盖「manifest 少声明」
2133
+ // 方向;**manifest 多声明**(声明支持而引擎实际不支持)由首个 run 的协议握手
2134
+ // `initialize` 发现 → engine_capability_mismatch 该 run 失败 + record 标 failed,
2135
+ // 并**清理 run 前已建的前置副作用**(worktree 经 finalizeFailed → finalizeRecord
2136
+ // Step 3b cleanupWorktreeIfBound 清理)。非 gate 位不一致(无论强弱)一律
2137
+ // warn 留痕不阻断(诊断面归 EngineClient,设计 §3.3 能力位段)。
1601
2138
  assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
1602
2139
 
1603
2140
  // identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
@@ -1668,13 +2205,13 @@ export class SubagentService {
1668
2205
  }
1669
2206
 
1670
2207
  if (isPiRoute) {
1671
- // pi:record 耦合执行(runSpawn 直驱 record 记账)——预备轮次经 EnginePort 交接
1672
- //(kickOffChatRound),编排收尾(notify/终态迁移)与旧 pi 主路径语义一致。
2208
+ // pi:chat 轮次经协议 run(会话形态 chat{recordId})发往 pi-subagent-cli 引擎
2209
+ // 进程(W3——与 run 域 remote-engine 路由同构),编排收尾(notify/终态迁移)
2210
+ // 与旧 pi 主路径语义一致。
1673
2211
  this.kickOffChatRound(
1674
2212
  record,
1675
2213
  { ...recordOpts, worktree: worktreeHandle },
1676
2214
  identity,
1677
- this.buildSessionRunnerContext(opts.cwd),
1678
2215
  record.controller!.signal,
1679
2216
  PRIORITY_BACKGROUND,
1680
2217
  );
@@ -1695,6 +2232,8 @@ export class SubagentService {
1695
2232
  */
1696
2233
  private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
1697
2234
  const signal = record.controller?.signal;
2235
+ // [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
2236
+ this.roundSupervisor.noteRunStarted(record.id);
1698
2237
  void (async () => {
1699
2238
  try {
1700
2239
  await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
@@ -1711,13 +2250,17 @@ export class SubagentService {
1711
2250
  // DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
1712
2251
  // maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
1713
2252
  try {
1714
- await this.runEngineTask(record, opts, engine, signal);
2253
+ // [W4] adopted = 走了表 3 行 1 接管分支(record 保持 resumable 交监督器)——
2254
+ // 跳过 bg 完成回注(合并单条通知由监督器 sendMergedFailureNotice 承担,
2255
+ // route 会再发一条 toNotifyRecord 投影通知 = 双通知,正是 R3 要消除的时序窗口)。
2256
+ const adopted = await this.runEngineTask(record, opts, engine, signal);
1715
2257
  // cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
1716
- if (record.closedReason !== "cancelled") {
1717
- this.notifyHost.notifyComplete(record);
2258
+ if (!adopted && record.closedReason !== "cancelled") {
2259
+ this.collectCoordinator.route(record);
1718
2260
  }
1719
2261
  } finally {
1720
2262
  this.pool.release();
2263
+ this.roundSupervisor.noteRunEnded(record.id);
1721
2264
  }
1722
2265
  })();
1723
2266
  }
@@ -1733,7 +2276,7 @@ export class SubagentService {
1733
2276
  opts: ExecuteOptions,
1734
2277
  engine: EnginePort,
1735
2278
  signal: AbortSignal | undefined,
1736
- ): Promise<void> {
2279
+ ): Promise<boolean> {
1737
2280
  // [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
1738
2281
  //(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
1739
2282
  // 不传 forwardEvents。
@@ -1784,15 +2327,49 @@ export class SubagentService {
1784
2327
  journalPath: journal.path,
1785
2328
  };
1786
2329
  await journal.close();
1787
- await this.finalizeEngineOutcome(record, outcome);
2330
+ return await this.finalizeEngineOutcome(record, outcome);
1788
2331
  } catch (err) {
1789
2332
  // engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
1790
2333
  // journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
1791
2334
  await journal.close();
2335
+ // [W4 表 3 行 1] 引擎进程死亡(engine_crashed——run 帧已受理后进程死亡/stdin
2336
+ // 写失败)且宿主存活 → record 保持 resumable 交监督器接管(禁 completed 谎报、
2337
+ // 禁直接 closed 终局——resume 锚点在盘,逻辑任务可续)。prepare 期失败
2338
+ // (handshake/protocol/model 拒——进程创建前)维持现状 closed(确定性失败,
2339
+ // 保持 resumable 无意义)。
2340
+ if (
2341
+ err instanceof EngineSdkError &&
2342
+ err.code === "engine_crashed" &&
2343
+ record.chatMode !== true &&
2344
+ record.status === "running"
2345
+ ) {
2346
+ this.adoptResumableAfterEngineDeath(record, toErrorMessage(err));
2347
+ return true;
2348
+ }
1792
2349
  await this.finalizeFailed(record, err);
2350
+ return false;
1793
2351
  }
1794
2352
  }
1795
2353
 
2354
+ /**
2355
+ * [W4 表 3 行 1] 引擎/子进程死亡、宿主存活的 record 处置:run 终态如实记 failed
2356
+ * 证据(record.error),record **保持 resumable**(session 文件在盘,逻辑任务可续
2357
+ * ——冷路径 resume 可直接续写),交监督器接管(合并单条通知 + 三态判定)。禁止
2358
+ * 两个事故方向:completed 谎报(G3)与 closed 直接终局(resume 可能性丢失,等待
2359
+ * 无主——2026-09-08 事故环 2/3 的 core 侧形态)。
2360
+ *
2361
+ * 判据状态源钉死 record 级:写点只动 record 字段(resumable/result/error),
2362
+ * 不清镜像不查引擎——引擎进程被动重建(ensureConnected 退避重填镜像)不翻转
2363
+ * 本处置(纳管模型:死亡事件纳管、重建不解管)。
2364
+ */
2365
+ private adoptResumableAfterEngineDeath(record: ExecutionRecord, errMsg: string): void {
2366
+ record.error = errMsg;
2367
+ record.result = undefined;
2368
+ record.resumable = true;
2369
+ this.store.reportRecordTransition(record);
2370
+ this.roundSupervisor.adoptOnProcessDeath(record, errMsg);
2371
+ }
2372
+
1796
2373
  /**
1797
2374
  * 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
1798
2375
  * 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
@@ -1804,43 +2381,50 @@ export class SubagentService {
1804
2381
  /**
1805
2382
  * engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
1806
2383
  * 否则 done(result=content)。CAS 抢锁(tryTransition)防与 cancelBackground 双收尾。
2384
+ *
2385
+ * [W4 表 3 行 1] 进程死亡分诊:outcome.error 存在且 **exitCode === null**(引擎侧
2386
+ * 进程被信号终止/崩溃的合成 outcome 形态——RemoteEngine 运行中失败合成分支恒
2387
+ * exitCode:null,引擎如实上报的 turn 失败带数值 exitCode;engine-client 注释同源
2388
+ * 「exitCode null = 被信号杀死,杀链判据」)且宿主存活且非 conversation 形态 →
2389
+ * record 保持 resumable 交监督器接管(如实 failed 证据 + 合并通知 + 三态判定),
2390
+ * 不再 closed 终局。返回 true = 走了接管分支(调用方跳过 bg 完成回注)。
1807
2391
  */
1808
- private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<void> {
2392
+ private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
1809
2393
  if (outcome.sessionFile !== undefined) {
1810
2394
  record.sessionFile = outcome.sessionFile;
1811
2395
  }
1812
- const result: AgentResult = {
1813
- text: outcome.content,
1814
- turns: outcome.usage?.turns ?? 0,
1815
- durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
1816
- success: outcome.error === undefined,
1817
- ...(outcome.error !== undefined ? { error: outcome.error } : {}),
1818
- sessionId: outcome.sessionId ?? record.id,
1819
- toolCalls: [],
1820
- ...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
1821
- };
2396
+ if (
2397
+ outcome.error !== undefined &&
2398
+ outcome.exitCode === null &&
2399
+ record.chatMode !== true &&
2400
+ record.status === "running"
2401
+ ) {
2402
+ this.adoptResumableAfterEngineDeath(record, outcome.error);
2403
+ return true;
2404
+ }
2405
+ const result = this.outcomeToAgentResult(record, outcome);
1822
2406
  if (tryTransition(record, "closed", "gc")) {
1823
2407
  await this.finalizeRecord(record, result, "closed", "gc");
1824
2408
  }
2409
+ return false;
1825
2410
  }
1826
2411
 
1827
- // ── 执行内部:run + finalize(sync/bg 共用)──────────────
2412
+ // ── 执行内部:run + finalize(workflow 域 sync 面;chat 域轮次走 kickOffChatRound)──
1828
2413
 
1829
- /** 共享的"干活 + 收尾"——sync 直接 await,background 在 detached 里调。 */
2414
+ /**
2415
+ * workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
2416
+ * runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
2417
+ * 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
2418
+ */
1830
2419
  private async runAndFinalize(
1831
2420
  record: ExecutionRecord,
1832
2421
  opts: ExecuteOptions,
1833
- ctx: SessionRunnerContext,
1834
2422
  identity: ResolvedIdentity,
1835
2423
  signal: AbortSignal | undefined,
1836
2424
  priority: number,
1837
- rawOnEvent?: (event: AgentEvent) => void,
2425
+ onEvent?: (event: AgentEvent) => void,
1838
2426
  stream?: SubagentStream,
1839
- /** resume 选项(M2-B1):透传 runSpawn,重开已 idle 的 session 续聊。undefined = 新 session。 */
1840
- resume?: SpawnResumeOpts,
1841
2427
  ): Promise<AgentResult> {
1842
- // [U04 阶段化提取] 主函数只留编排,按「装配(池槽/worktree/深度)→ 执行(ALS 包装
1843
- // spawn)→ 回收(finally)→ 错误收口 → 终态收口」分段;行为逐字节不变。
1844
2428
  const pooled = record.mode === "background";
1845
2429
  let acquired = false;
1846
2430
  if (pooled) {
@@ -1848,48 +2432,106 @@ export class SubagentService {
1848
2432
  if (acquireFailure !== undefined) return acquireFailure;
1849
2433
  acquired = true;
1850
2434
  }
1851
- // onEvent 直通(原此处曾有 onUpdate(project(record)) 节流回流包装——生产死路径,
1852
- // 三调用点恒 onUpdate: undefined、仅测试触达,已按 swf-perf-impl ledger #22 删除
1853
- // (决策 TC4/IF14,详见 .cw/swf-perf-impl/cleanup-slice-design.json;git 历史可完整恢复)。
1854
- // ExecuteOptions.onUpdate 字段一并删除,未来误用将编译期失败而非静默无效。
1855
- const onEvent = rawOnEvent;
1856
2435
 
1857
- const worktreeHandle = this.resolveWorktreeHandle(opts);
1858
- const { parentDepth, effectiveDepth } = this.resolveForkDepths(opts);
2436
+ // worktree 句柄经 executeOptionsToEngineTaskSpec(opts).worktree 直传引擎
2437
+ //(AgentCallOpts.worktree 接受 WorktreeHandle——原 runSpawn 显式传参的协议形态)。
2438
+ const engine = this.resolveChatEnginePort();
2439
+ // journal 接线(D6 第②级,与 kickOffEngineRun 同一实现):forwardEvents = onEvent
2440
+ //(workflow liveRecord 桥接,D-A8)。
2441
+ const journal = wireEventJournal({ engineId: engine.id, taskId: record.id, forwardEvents: onEvent });
1859
2442
 
1860
2443
  let result: AgentResult;
1861
2444
  try {
1862
- result = await this.runSpawnNested(
1863
- record, opts, ctx, identity, signal, onEvent, stream, resume,
1864
- worktreeHandle, parentDepth, effectiveDepth,
1865
- );
2445
+ const runCtx: RunContext = {
2446
+ taskId: record.id,
2447
+ poolKey: JOURNAL_INITIAL_POOL_KEY,
2448
+ signal,
2449
+ ctxModel: identity.resolved.model,
2450
+ onEvent: journal.onEvent,
2451
+ onPoolResolved: journal.onPoolResolved,
2452
+ ...(stream !== undefined ? { stream } : {}),
2453
+ ...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
2454
+ // D10 终止链:引擎 spawn 的子进程注册进 spawnedChildren 镜像记账
2455
+ onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
2456
+ };
2457
+ const { handle, outcome } = await engine.run(this.taskSpecWithModel(opts, record.model), runCtx);
2458
+ record.engineHandle = {
2459
+ sessionRef: handle.data.sessionRef,
2460
+ poolKey: handle.data.poolKey,
2461
+ journalPath: journal.path,
2462
+ };
2463
+ await journal.close();
2464
+ result = this.outcomeToAgentResult(record, outcome);
1866
2465
  } catch (err) {
1867
- // run() 正常路径不抛错,但创建期异常(createAndConfigureSession 失败)
1868
- // 会逃逸出 run() —— 合成 failed result + 收尾。
1869
- // swallow(不 re-throw):sync 调用方拿到合成 failed result,background 的
1870
- // .then 正常跑 notify。避免异常逃逸到 tool 层 + record 卡 running。
2466
+ await journal.close();
2467
+ // engine.run prepare 期 reject(进程创建前)→ 合成 failed result + 收尾。
2468
+ // swallow(不 re-throw):sync 调用方拿到合成 failed result,避免异常逃逸到
2469
+ // tool 层 + record 卡 running。
1871
2470
  if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
1872
2471
  return this.finalizeFailed(record, err);
1873
2472
  } finally {
1874
2473
  this.releaseRoundResources(record, pooled && acquired, stream);
1875
2474
  }
1876
2475
 
1877
- // [V2 决策 2/3] chatMode 首轮闭环:runSpawn 因 agent_settled 提前 resolve(onRoundSettled
1878
- // 已设 record.status=idle + round+=1 + notify 主 agent)。进程仍保活(idle timer armed),
1879
- // 不进下方 chatMode 分流(那是 close 后 done/failed/cancelled 终态化的,走 finalizeRoundToIdle
1880
- // / finalizeRecord)。tryTransition(idle→done) 天然失败(要求 status==="running"),此处显式
1881
- // early return 让语义清晰 + 防状态机未来改动。防 double-notify 由 notifier dedup 兜底
1882
- //(同 id:round 60s 内吞,chat 轮次收尾 .then 的 notify 是 no-op,见 notifier.ts L122)。
1883
- if (record.chatMode && isIdle(record)) {
1884
- return result;
1885
- }
1886
-
1887
2476
  // v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
1888
- const aborted = signal?.aborted === true;
1889
- await this.settleFinalOutcome(record, result, aborted, this.deriveClosedReason(aborted, result.success));
2477
+ await this.settleOneShotOutcome(record, result, signal?.aborted === true);
1890
2478
  return result;
1891
2479
  }
1892
2480
 
2481
+ /** engine.run taskSpec 装配单一来源(runAndFinalize 与 kickOffChatRound 共用):
2482
+ * executeOptions 协议映射 + model = record 留痕词形(resolved 解析产物,
2483
+ * joinEngineModelRef 规范形)覆盖——原 identity.resolved 经 runSpawn --model
2484
+ * 兜底的协议等价承载。 */
2485
+ private taskSpecWithModel(opts: ExecuteOptions, model: string | undefined): AgentCallOpts {
2486
+ return { ...executeOptionsToEngineTaskSpec(opts), ...(model !== undefined ? { model } : {}) };
2487
+ }
2488
+
2489
+ /**
2490
+ * one-shot(非 chatMode)终态收口(原 settleOneShotOutcome 分支):成功轮消费
2491
+ * closeAfterRound 挂起标志;失败/取消一次性销毁。CAS 抢锁失败(cancel/dispose 抢先
2492
+ * 终态化)静默跳过。runAndFinalize(workflow 域)与 kickOffChatRound 非 chatMode
2493
+ * 分支共用。
2494
+ */
2495
+ private async settleOneShotOutcome(
2496
+ record: ExecutionRecord,
2497
+ result: AgentResult,
2498
+ aborted: boolean,
2499
+ ): Promise<void> {
2500
+ if (!tryTransition(record, "closed", aborted ? "cancelled" : result.success ? "user-close" : "gc")) return;
2501
+ if (!aborted && result.success && record.closeAfterRound === true) {
2502
+ // [M5] busy 时 close(force:false) 置的标志在本轮完成时消费终态化。
2503
+ await this.consumeCloseAfterRound(record, result, "user-close");
2504
+ } else if (!aborted && result.success) {
2505
+ // [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
2506
+ await this.finalizeRoundToIdle(record, result);
2507
+ } else if (!aborted && record.closeAfterRound === true) {
2508
+ // [M5] 优雅关闭挂起的失败轮:轮已完成即兑现 close 意图终态化(含本轮 result)。
2509
+ await this.consumeCloseAfterRound(record, result, "gc");
2510
+ } else {
2511
+ // 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
2512
+ await this.finalizeRecord(record, result, "closed", aborted ? "cancelled" : "gc");
2513
+ }
2514
+ }
2515
+
2516
+ /** AgentOutcome → execution AgentResult 单一映射源(workflow run 域映射;
2517
+ * finalizeEngineOutcome 终态 result 复用此处构造——exitCode null = 被信号杀死的
2518
+ * 合成终态,error 如实透传)。 */
2519
+ private outcomeToAgentResult(record: ExecutionRecord, outcome: AgentOutcome): AgentResult {
2520
+ if (outcome.sessionFile !== undefined) {
2521
+ record.sessionFile = outcome.sessionFile;
2522
+ }
2523
+ return {
2524
+ text: outcome.content,
2525
+ turns: outcome.usage?.turns ?? 0,
2526
+ durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
2527
+ success: outcome.error === undefined,
2528
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
2529
+ sessionId: outcome.sessionId ?? record.id,
2530
+ toolCalls: [],
2531
+ ...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
2532
+ };
2533
+ }
2534
+
1893
2535
  /** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
1894
2536
  * 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
1895
2537
  * 不触发轮次资源回收——与原控制流逐字节一致)。 */
@@ -1913,69 +2555,16 @@ export class SubagentService {
1913
2555
  return typeof opts.worktree === "object" ? opts.worktree : undefined;
1914
2556
  }
1915
2557
 
1916
- /** [U04 提取·装配] [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。 */
1917
- private resolveForkDepths(opts: ExecuteOptions): { parentDepth: number; effectiveDepth: number } {
1918
- const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
1919
- const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
1920
- return { parentDepth, effectiveDepth };
1921
- }
1922
-
1923
- /** [U04 提取·执行] runSpawn 的两层 ALS 包装(forkDepthAls 外层 + execNesting 内层)。 */
1924
- private runSpawnNested(
1925
- record: ExecutionRecord,
1926
- opts: ExecuteOptions,
1927
- ctx: SessionRunnerContext,
1928
- identity: ResolvedIdentity,
1929
- signal: AbortSignal | undefined,
1930
- onEvent: ((event: AgentEvent) => void) | undefined,
1931
- stream: SubagentStream | undefined,
1932
- resume: SpawnResumeOpts | undefined,
1933
- worktreeHandle: WorktreeHandle | undefined,
1934
- parentDepth: number,
1935
- effectiveDepth: number,
1936
- ): Promise<AgentResult> {
1937
- // 嵌套上下文包在 forkDepthAls 内层:B run() 期间挂 {recordId:B.id,depth:B.depth},
1938
- // B 内创建 C 时 createRecordForMode 读到 B → C 挂到 B 名下。两层 ALS 独立但同生命周期。
1939
- return this.forkDepthAls.run(effectiveDepth, () =>
1940
- this.execNesting.run(
1941
- { recordId: record.id, depth: record.depth },
1942
- () => runSpawn(record, opts.task, {
1943
- resolved: identity.resolved,
1944
- agentConfig: identity.agentConfig,
1945
- appendSystemPrompt: opts.appendSystemPrompt,
1946
- skillPath: opts.skillPath,
1947
- schema: opts.schema,
1948
- schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
1949
- maxTurns: opts.maxTurns,
1950
- graceTurns: opts.graceTurns,
1951
- signal,
1952
- onEvent,
1953
- stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
1954
- fork: opts.fork,
1955
- // [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
1956
- // opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
1957
- forkSource: opts.forkFromSessionFile,
1958
- worktree: worktreeHandle,
1959
- parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
1960
- }, ctx, resume),
1961
- ),
1962
- );
1963
- }
1964
-
1965
2558
  /** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
1966
- * 成功)、streaming widget 清除、在途 resume 守卫清除。 */
2559
+ * 成功)、streaming widget 清除。 */
1967
2560
  private releaseRoundResources(
1968
- record: ExecutionRecord,
2561
+ _record: ExecutionRecord,
1969
2562
  holdSlot: boolean,
1970
2563
  stream: SubagentStream | undefined,
1971
2564
  ): void {
1972
2565
  if (holdSlot) this.pool.release();
1973
2566
  // 清除 streaming widget(subagent 终态,幂等)
1974
2567
  stream?.dispose();
1975
- // [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
1976
- // MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
1977
- // execute() 新建 record 不在集合,delete 是 no-op。
1978
- this.resumesInFlight.delete(record.id);
1979
2568
  }
1980
2569
 
1981
2570
  /** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
@@ -1992,6 +2581,7 @@ export class SubagentService {
1992
2581
  sessionId: record.id,
1993
2582
  toolCalls: [],
1994
2583
  };
2584
+ disarmRoundFromProtocol(record.id);
1995
2585
  if (tryTransition(record, "closed", "gc")) {
1996
2586
  // 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
1997
2587
  await this.finalizeRoundToIdle(record, failedResult);
@@ -1999,81 +2589,6 @@ export class SubagentService {
1999
2589
  return failedResult;
2000
2590
  }
2001
2591
 
2002
- /** [U04 提取·收口派生] closedReason 派生:aborted → cancelled;否则 success → user-close,!success → gc。 */
2003
- private deriveClosedReason(aborted: boolean, success: boolean): ClosedReason {
2004
- return aborted ? "cancelled" : success ? "user-close" : "gc";
2005
- }
2006
-
2007
- /** [U04 提取·终态收口] CAS 抢锁:抢到则按 chatMode 分流完整收尾;没抢到(cancel 已先设
2008
- * closed+cancelled)则跳过。 */
2009
- private async settleFinalOutcome(
2010
- record: ExecutionRecord,
2011
- result: AgentResult,
2012
- aborted: boolean,
2013
- closedReason: ClosedReason,
2014
- ): Promise<void> {
2015
- if (!tryTransition(record, "closed", closedReason)) return;
2016
- if (record.chatMode) {
2017
- await this.settleChatRoundOutcome(record, result, aborted, closedReason);
2018
- } else {
2019
- await this.settleOneShotOutcome(record, result, aborted, closedReason);
2020
- }
2021
- }
2022
-
2023
- /** [U04 提取·终态收口] chatMode 轮次分流(原 A/B 分支,De Morgan 等价拆分):
2024
- * 成功轮(!aborted && success)与失败/取消轮(其补集 !success || aborted)各自处理
2025
- * closeAfterRound 挂起语义。 */
2026
- private async settleChatRoundOutcome(
2027
- record: ExecutionRecord,
2028
- result: AgentResult,
2029
- aborted: boolean,
2030
- closedReason: ClosedReason,
2031
- ): Promise<void> {
2032
- if (!aborted && result.success) {
2033
- if (record.closeAfterRound) {
2034
- // close 优雅关闭(force:false):当前轮完成后终态化为 closed。
2035
- await this.consumeCloseAfterRound(record, result, "user-close");
2036
- } else {
2037
- // 对话模式轮次成功完成 → 保持 running(旧 idle 折入 running,finalizeRoundToIdle 设回 running)。
2038
- await this.finalizeRoundToIdle(record, result);
2039
- }
2040
- } else {
2041
- // MF-6:chatMode 轮次失败/取消不销毁对话——回退 running-resumable(旧 idle,可恢复)。
2042
- if (record.closeAfterRound) {
2043
- // [M5] 优雅关闭挂起的失败/取消轮:轮已完成即兑现 close 意图终态化(含本轮 result),
2044
- // 不再回退 resumable——否则标志残留到下一轮,record 已被 tool 谎报 closed。
2045
- await this.consumeCloseAfterRound(record, result, closedReason);
2046
- } else {
2047
- await this.finalizeRoundToIdle(record, result);
2048
- }
2049
- }
2050
- }
2051
-
2052
- /** [U04 提取·终态收口] one-shot(非 chatMode)分流(原 C/D 分支):成功轮消费
2053
- * closeAfterRound 挂起标志;失败/取消一次性销毁。 */
2054
- private async settleOneShotOutcome(
2055
- record: ExecutionRecord,
2056
- result: AgentResult,
2057
- aborted: boolean,
2058
- closedReason: ClosedReason,
2059
- ): Promise<void> {
2060
- if (!aborted && result.success) {
2061
- if (record.closeAfterRound) {
2062
- // [M5] 非 chatMode(one-shot)busy 时 close(force:false) 置的标志在本轮完成时消费
2063
- // 终态化(对齐 close schema 文案 "release its resources")。旧代码走
2064
- // finalizeRoundToIdle 不消费——tool 返回 {closed:true} 谎报,record 永久
2065
- // running-resumable、5min idle timer 杀进程、期间还能继续收 message。
2066
- await this.consumeCloseAfterRound(record, result, "user-close");
2067
- } else {
2068
- // [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
2069
- await this.finalizeRoundToIdle(record, result);
2070
- }
2071
- } else {
2072
- // 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
2073
- await this.finalizeRecord(record, result, "closed", closedReason);
2074
- }
2075
- }
2076
-
2077
2592
  /** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
2078
2593
  * 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
2079
2594
  private async consumeCloseAfterRound(
@@ -2086,13 +2601,15 @@ export class SubagentService {
2086
2601
  }
2087
2602
 
2088
2603
  /**
2089
- * pi chat 域轮次的 detached 编排(D2 单轨——旧 pi 主路径绕过 EnginePort 的 detached
2090
- * 编排删除后的 EnginePort 化形态):
2091
- * 预备轮次(record/opts/identity/host ctx/stream/resume)挂载交接 Map engine.run
2092
- * ctx.taskId 消费(PiEngine chat 分支回调 runChatRound——即 runAndFinalize 链,含 pool
2093
- * 并发槽 acquire/release 与杀链,行为零变化)→ 完成回注 notify(cancel 抢先时跳过,
2094
- * 与旧 .then 语义一致)。
2095
- * stream 在挂载前同步创建(spawn 前置不变量,stream-sink 退役步骤 2 守护)。
2604
+ * pi chat 域轮次的 detached 编排([W3 协议形态]):轮次经协议 run(会话形态
2605
+ * chat{recordId, resume?})发往 pi-subagent-cli 引擎进程——
2606
+ * - 应答时点 = 首轮 agent_settled(W2 契约:idle 帧先于应答帧;outcome = 本轮
2607
+ * 内容非会话终态)→ 应答到达即本轮 settle(round+1 / 增量通知 / closeAfterRound
2608
+ * 消费,见 settleChatRoundFromResponse);
2609
+ * - 流式 delta / 轮次生命周期经 host/streamDelta + host/roundLifecycle 反向通道
2610
+ * 回流:首轮 runId 键(RunContext.stream / onRoundLifecycle)、续聊轮 recordId
2611
+ * 键(chatRoundRoutes 注册的 recordId 路由);
2612
+ * - recordId 路由在本方法注册(首轮 + 冷续轮替换式重注册),record 终态化路径注销。
2096
2613
  * chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
2097
2614
  * 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
2098
2615
  */
@@ -2100,99 +2617,356 @@ export class SubagentService {
2100
2617
  record: ExecutionRecord,
2101
2618
  opts: ExecuteOptions,
2102
2619
  identity: ResolvedIdentity,
2103
- ctx: SessionRunnerContext,
2104
2620
  signal: AbortSignal | undefined,
2105
2621
  priority: number,
2106
- /** resume 选项(M2-B1):透传轮次执行→runSpawn。undefined = 新 session。 */
2107
- resume?: SpawnResumeOpts,
2622
+ /** 冷续锚点(M2-B1 协议形态):run.params.chat.resume。undefined = 新 session。 */
2623
+ resume?: ResumeAnchor,
2108
2624
  ): void {
2109
2625
  // 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
2110
2626
  // 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
2111
- const stream = createBackgroundStream(record.id, this.streamSink, ctx.mode, process.env);
2112
-
2113
- this.chatRoundTickets.set(record.id, { record, opts, identity, ctx, signal, priority, stream, resume });
2114
- void this.chatPiEngine
2115
- .run(
2116
- // task 形参仅满足 port 签名——chat 轮次由 ticket lossless 携带
2117
- //(identity/forkFromSessionFile/host ctx host 编排件,不入任务声明
2118
- // AgentCallOpts——[u-3b D6] 双形态保留的结构性裁决见 ChatRoundTicket 注释)
2119
- executeOptionsToEngineTaskSpec(opts),
2120
- { taskId: record.id, poolKey: PI_POOL_KEY, signal, stream },
2121
- )
2122
- .then(() => {
2627
+ const stream = createBackgroundStream(record.id, this.streamSink, this.uiObservability.getMode(), process.env);
2628
+
2629
+ // 注册 recordId 键反向通道路由(替换式——冷续轮重注册):interact 续聊轮的
2630
+ // streamDelta / roundLifecycle 分发目标;首轮流式 delta 走 ctx.stream(runId 键),
2631
+ // 本路由的 onStreamDelta 承担中段守护刷新 + dispose 后的续轮 delta(幂等 no-op)。
2632
+ // chat 会话形态注册(一次性 run 无反向轮次面)。
2633
+ const prevUnregister = this.chatRoundRoutes.get(record.id);
2634
+ prevUnregister?.();
2635
+ const engine = this.resolveChatEnginePort();
2636
+ if (record.chatMode) {
2637
+ this.chatRoundRoutes.set(
2638
+ record.id,
2639
+ engine.registerChatRoundRoute?.(record.id, {
2640
+ onStreamDelta: (delta) => {
2641
+ refreshFromProtocolEvent(record.id);
2642
+ stream?.onDelta(delta);
2643
+ },
2644
+ onRoundLifecycle: (phase) => this.handleChatRoundPhase(record, phase),
2645
+ }) ?? (() => {}),
2646
+ );
2647
+ }
2648
+
2649
+ // [W4] 冷路径 resume 轮的在途记账:死亡纳管 record 被主 agent resume = 决策收敛
2650
+ // (清指引标记与看门狗,回归「该等」)。热路径轮(deliverChatMessage)不经此处,
2651
+ // 其「该等」由 hasLiveProcess 判据覆盖(进程活)——conversation 形态本就豁免监督域。
2652
+ this.roundSupervisor.noteRunStarted(record.id);
2653
+ void (async () => {
2654
+ try {
2655
+ await this.pool.acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
2656
+ } catch {
2657
+ // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
2658
+ if (signal?.aborted) {
2659
+ await this.finalizeAborted(record);
2660
+ } else {
2661
+ await this.finalizeFailed(record, new Error("aborted"));
2662
+ }
2663
+ return;
2664
+ }
2665
+ try {
2666
+ // [F-2 首轮/冷续轮 arm 重接] 轮开跑(pool 槽已到手、run 协议帧即将派发)挂
2667
+ // **中段**无进展检测——被删的 inproc stdout-pump 是首轮唯一中段守护,协议化后
2668
+ // 引擎侧 spawn-runner 仅 turn 计数无墙钟,本 arm 是首轮 wedged 的唯一熔断
2669
+ // (LC-1 场景①:pi 无事件行输出)。refresh 源(对照 deliverChatMessage 热路径
2670
+ // 的 arm/refresh 形态,同一守护实例语义):① runId 键协议事件行(ctx.onEvent,
2671
+ // 含 text_delta——引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta
2672
+ // 两通道,刷新面与热路径 streamDelta 等效);② settled 相位交棒
2673
+ // (ctx.onRoundLifecycle → handleChatRoundPhase → noteRoundSettledFromProtocol,
2674
+ // 首轮 runId 键同样生效);idle 相位 disarmRoundFromProtocol 两段一并清。
2675
+ // arm 置于 acquire 之后:排队窗口不计入 no-progress 静默(窗语义 = 轮开跑后)。
2676
+ armMidRoundNoProgress(record.id, {
2677
+ onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
2678
+ onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
2679
+ });
2680
+ // 协议 run:chatMode = 会话形态(chat.recordId = 关联键;resume 存在 = 冷续;
2681
+ // 应答时点 = 首轮 agent_settled,本轮 settle / 守护交棒 / idle 定时器挂载由
2682
+ // handleChatRoundPhase + settleChatRoundFromResponse 承接);非 chatMode =
2683
+ // 一次性 run(协议面无 chat 键,终态语义对齐原 settleOneShotOutcome)。
2684
+ const { outcome } = await engine.run(
2685
+ // resume 锚点轮引擎侧覆盖 model 解析(taskSpec 装配单一来源见 taskSpecWithModel)。
2686
+ this.taskSpecWithModel(opts, record.model),
2687
+ {
2688
+ taskId: record.id,
2689
+ poolKey: PI_POOL_KEY,
2690
+ signal,
2691
+ ...(stream !== undefined ? { stream } : {}),
2692
+ ctxModel: identity.resolved.model,
2693
+ // chat 会话形态参数(conversation 形态必传;冷续带锚点);一次性 run 不携带。
2694
+ ...(record.chatMode
2695
+ ? {
2696
+ chat: {
2697
+ recordId: record.id,
2698
+ ...(resume !== undefined ? { resume } : {}),
2699
+ },
2700
+ // 首轮 runId 键生命周期帧:守护交棒 / idle 定时器挂载 / failed 分诊。
2701
+ onRoundLifecycle: (phase: HostRoundLifecycleParams) => this.handleChatRoundPhase(record, phase),
2702
+ // 中段守护刷新源①:首轮协议事件行(message_*/tool_*/turn_end——有效
2703
+ // 事件到达即刷新;chat 域不接 journal,事件仅作活性信号消费)。
2704
+ onEvent: () => refreshFromProtocolEvent(record.id),
2705
+ }
2706
+ : {}),
2707
+ },
2708
+ );
2709
+ if (outcome.sessionFile !== undefined) record.sessionFile = outcome.sessionFile;
2710
+ const result = this.outcomeToAgentResult(record, outcome);
2711
+ if (record.chatMode) {
2712
+ // 本轮 settle(应答 = 首轮 agent_settled):round+1 / 增量通知 / closeAfterRound 消费。
2713
+ this.settleChatRoundFromResponse(record, outcome);
2714
+ } else {
2715
+ // 一次性 run 终态收口(对齐原 settleOneShotOutcome:成功轮 SP-5 回退
2716
+ // running-resumable 等待 upgrade;失败/取消一次性销毁)。
2717
+ await this.settleOneShotOutcome(record, result, signal?.aborted === true);
2718
+ }
2123
2719
  // background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
2124
2720
  // 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
2125
2721
  // 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
2126
2722
  // 表达)——迟到的完成回注不注入(可能已切换的)新 session。
2127
2723
  if (notifyGateAllowsDelivery(record.closedReason)) {
2128
- this.notifyHost.notifyComplete(record);
2724
+ this.collectCoordinator.route(record);
2725
+ }
2726
+ } catch (err) {
2727
+ // 轮次 run 失败(prepare 期 reject / 引擎进程死亡 / cancel 后未收敛合成终态):
2728
+ // chatMode MF-6——不销毁对话,回退可恢复(session 文件在盘,冷续 run 接续);
2729
+ // 非 chatMode 终态销毁(finalizeFailed)。cancel 抢先时 record 已终态化
2730
+ //(tryTransition 失败跳过),此处仅吞错。
2731
+ if (record.chatMode) {
2732
+ await this.finalizeChatSpawnFailure(record, err);
2733
+ } else {
2734
+ await this.finalizeFailed(record, err);
2129
2735
  }
2130
- })
2131
- .catch((err: unknown) => {
2132
- // detached 吞错:轮次执行内部已 finalize record(含 emitPendingUnregister),
2133
- // 且 finalizeRecord 的 manifest 写入已降级为 best-effort(失败仅 logger.error + appendEntry,
2134
- // 不外抛)。因此此处不应走到——但作为最后一道兼底,记录调试日志后吞下,不外抛。
2135
- // 完成通知由 finalizeRecord 内的 emitPendingUnregister 承担(pending-notifications 消费)。
2136
- // cancel 抢先时 status=cancelled,cancelBackground 自己 emit,此处无需重复。
2137
- // 交接包未被消费(engine.run 前置 throw 的极端形态)时的泄漏兜底。
2138
- this.chatRoundTickets.delete(record.id);
2139
2736
  if (err instanceof Error) {
2140
- logger.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
2737
+ logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
2141
2738
  }
2142
- });
2739
+ } finally {
2740
+ this.pool.release();
2741
+ // 在途 resume 守卫清除(幂等;冷续轮收尾)+ streaming widget 清除(轮终,幂等——
2742
+ // 续轮 delta 落已 dispose 的 stream 为 no-op,与 inproc 形态逐点一致)。
2743
+ this.resumesInFlight.delete(record.id);
2744
+ stream?.dispose();
2745
+ // [W4] 轮收口重评估(死亡纳管 record 的轮终 → 驱动可能又死 → 重新三态判定)。
2746
+ this.roundSupervisor.noteRunEnded(record.id);
2747
+ }
2748
+ })();
2143
2749
  }
2144
2750
 
2145
- /** PiEngine 的编排服务适配器(chat 绑定):闭包持有本实例的编排面——chat 轮次交接
2146
- * 由此成为 PiEngineService 的可选面(SAR 直绑 Service workflow 实例不提供,
2147
- * Service 公共接口不为引擎内部交接扩面)。 */
2148
- private piEngineServiceAdapter(): PiEngineService {
2751
+ /**
2752
+ * [W3] chat 轮次生命周期帧消费(host/roundLifecycle,runId 键首轮 / recordId 键续聊轮
2753
+ * 经本方法收敛)。相位 → 宿主编排语义(W4 协议事件面三入口):
2754
+ * - settled:轮收敛——settled-watchdog 中段让位收尾段(noteRoundSettledFromProtocol);
2755
+ * - idle:轮收口进 idle 稳态——两段守护一并清(disarmRoundFromProtocol)+ 冷续锚点
2756
+ * 回填(sessionFile/engineHandle,idle 帧先于 run 应答帧)+ **idle 定时器挂载**
2757
+ * (引擎不实现 idle 定时器——core arm,W2 交接契约);
2758
+ * - failed:轮异常终止(engine_round_aborted / engine_round_crashed /
2759
+ * engine_round_epipe_exhausted)——record 如实标 failed(禁 completed 谎报),
2760
+ * chatMode 按 MF-6 回退可恢复(onChatRoundFailed)。
2761
+ */
2762
+ private handleChatRoundPhase(record: ExecutionRecord, phase: HostRoundLifecycleParams): void {
2763
+ switch (phase.phase) {
2764
+ case "settled":
2765
+ noteRoundSettledFromProtocol(record.id);
2766
+ break;
2767
+ case "idle":
2768
+ disarmRoundFromProtocol(record.id);
2769
+ this.armChatIdleTimer(record);
2770
+ if (phase.anchor !== undefined) {
2771
+ this.backfillChatAnchor(record, phase.anchor);
2772
+ }
2773
+ break;
2774
+ case "failed":
2775
+ this.onChatRoundFailed(record, phase.error);
2776
+ break;
2777
+ }
2778
+ }
2779
+
2780
+ /**
2781
+ * [W3] idle 相位 = idle 定时器锚点(原 session-runner handleAgentSettledForChatMode
2782
+ * 的 armIdleTimer 段):挂载降级链(配置值 → DEFAULT 兜底 + warn)语义保持;超时
2783
+ * 处置 = 引擎侧进程回收(协议 interact close force)——idle timer SIGTERM 的协议形态。
2784
+ */
2785
+ private armChatIdleTimer(record: ExecutionRecord): void {
2786
+ const onTimeout = (): void => {
2787
+ killRecordChildWithEscalation(record.id, "idle timer");
2788
+ this.terminateChatSession(record, "close", "idle timer");
2789
+ };
2790
+ try {
2791
+ armIdleTimer(record.id, onTimeout, record.idleTimeoutMs);
2792
+ } catch (err) {
2793
+ bestEffort(err, "armIdleTimer (chat idle phase)", "error");
2794
+ try {
2795
+ armIdleTimer(record.id, onTimeout, DEFAULT_IDLE_TIMEOUT_MS);
2796
+ logger.warn(
2797
+ `[subagents] idleTimeoutMs invalid for ${record.id}, fell back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms) — idle GC and round notification gate stay active`,
2798
+ );
2799
+ } catch (fallbackErr) {
2800
+ bestEffort(fallbackErr, "armIdleTimer fallback (chat idle phase)", "error");
2801
+ }
2802
+ }
2803
+ }
2804
+
2805
+ /** [W3] idle 帧锚点回填(冷续锚点 = 引擎侧会话滚动/compaction 后的最新定位)。 */
2806
+ private backfillChatAnchor(
2807
+ record: ExecutionRecord,
2808
+ anchor: { sessionRef: Record<string, string>; poolKey: string; journalPath?: string },
2809
+ ): void {
2810
+ const sessionFile = anchor.sessionRef["sessionFile"];
2811
+ if (typeof sessionFile === "string" && sessionFile !== "") {
2812
+ record.sessionFile = sessionFile;
2813
+ }
2814
+ // engineHandle 回填(①级读取钥匙;幂等守卫——终态回填已落则不覆盖)。
2815
+ if (record.engineHandle === undefined || record.engineHandle.sessionRef["sessionId"] === undefined) {
2816
+ record.engineHandle = {
2817
+ sessionRef: { ...anchor.sessionRef },
2818
+ poolKey: anchor.poolKey,
2819
+ ...(anchor.journalPath !== undefined ? { journalPath: anchor.journalPath } : {}),
2820
+ };
2821
+ }
2822
+ this.store.reportRecordTransition(record);
2823
+ }
2824
+
2825
+ /**
2826
+ * [W3] chat 轮 failed 相位分诊:错误如实落 record(失败相位错误码——engine_round_
2827
+ * aborted(宿主 cancel/close)/ engine_round_crashed(子进程外部死亡)/
2828
+ * engine_round_epipe_exhausted(EPIPE 兜底耗尽)),禁 completed 谎报。chatMode 按
2829
+ * MF-6 不销毁对话——回退可恢复(finalizeRoundToIdle + 通知),冷续 run 接续;
2830
+ * record 已终态(cancel/close 抢先)= 迟到帧,幂等跳过。
2831
+ */
2832
+ private onChatRoundFailed(record: ExecutionRecord, error: ProtocolError): void {
2833
+ disarmRoundFromProtocol(record.id);
2834
+ if (record.status !== "running") return;
2835
+ const errMsg = `${error.code}: ${error.message}`;
2836
+ record.lastError = errMsg;
2837
+ const failedResult: AgentResult = {
2838
+ text: "",
2839
+ turns: record.turnCount,
2840
+ durationMs: Date.now() - record.startedAt,
2841
+ success: false,
2842
+ error: errMsg,
2843
+ sessionId: record.id,
2844
+ toolCalls: [],
2845
+ };
2846
+ void this.finalizeRoundToIdle(record, failedResult)
2847
+ .then(() => {
2848
+ if (notifyGateAllowsDelivery(record.closedReason)) {
2849
+ this.collectCoordinator.route(record);
2850
+ }
2851
+ })
2852
+ .catch((err: unknown) => bestEffort(err, "chat round failed finalize", "error"));
2853
+ }
2854
+
2855
+ /**
2856
+ * [W3] 首轮 run 应答到达(= 首轮 agent_settled,W2 契约)的本轮 settle:round+1 /
2857
+ * 轮次增量通知 / base 推进 / reportRecordTransition / closeAfterRound 消费——语义
2858
+ * 权威自持于此(原 round-settlement.ts createRoundSettler 已删,本闭包为其唯一后继),
2859
+ * 唯一偏差 = 轮次文本源:协议形态下 live turns 留在引擎进程内,core 以应答
2860
+ * outcome.content(W2「outcome = 本轮内容」)为增量权威,record.turns 派生不可用。
2861
+ * idle 定时器挂载不在此处——idle 相位帧先于应答帧到达,armChatIdleTimer 已锚定。
2862
+ */
2863
+ private settleChatRoundFromResponse(record: ExecutionRecord, outcome: AgentOutcome): void {
2864
+ if (record.status !== "running") return; // 终态(cancel/close 抢先)——迟到应答幂等跳过
2865
+ record.round = (record.round ?? 0) + 1;
2866
+ const roundText = outcome.content;
2867
+ record.result = roundText ||
2868
+ (record.lastError ? `round did not complete: ${record.lastError}` : "(no output this round)");
2869
+ // 先送达本轮增量(notify),再推进 base
2870
+ //(notify 后推进:notify 失败时 base 不推进,增量并入下一轮防丢文本)。
2871
+ // [T4①/PS-2] 通知门与 kickOff 回注同款:parent-new/parent-fork 编排性关闭后
2872
+ // 迟到的应答 settle 不注入(可能已切换的)新 session;cancelled 由 cancelBackground
2873
+ // 自行 notify。
2874
+ if (notifyGateAllowsDelivery(record.closedReason)) {
2875
+ this.collectCoordinator.route(record);
2876
+ }
2877
+ record.roundBaseTurnIndex = record.turnCount;
2878
+ this.store.reportRecordTransition(record);
2879
+ // [M5] closeAfterRound 消费:chatMode 轮次完成的统一汇聚点(优雅关闭在本轮
2880
+ // 完成时兑现终态化)。
2881
+ if (record.closeAfterRound) {
2882
+ record.closeAfterRound = undefined;
2883
+ void this.closeAfterRoundSettled(record);
2884
+ }
2885
+ }
2886
+
2887
+ /** [W3] chat 域引擎终止意图(协议面):cancel = interact cancel(引擎侧 SIGTERM →
2888
+ * settle 等待 → 杀链升级);close = interact close force(立即杀链收割)。fire-and-forget
2889
+ * ——失败 best-effort 留证(宿主组级收割兜底 = EngineClient killAll,dispose 路径)。 */
2890
+ private terminateChatSession(
2891
+ record: ExecutionRecord,
2892
+ kind: "cancel" | "close",
2893
+ source: string,
2894
+ ): void {
2895
+ void (async () => {
2896
+ const engine = this.resolveChatEnginePort();
2897
+ const handle = this.chatHandleFor(record);
2898
+ const result = kind === "cancel"
2899
+ ? await engine.interact(handle, { kind: "cancel" })
2900
+ : await engine.interact(handle, { kind: "close", payload: { force: true } });
2901
+ if (!result.ok) {
2902
+ logger.debug(
2903
+ `[subagents] terminateChatSession (${kind}, ${source}) rejected for ${record.id}: ${result.message}`,
2904
+ );
2905
+ }
2906
+ })().catch((err: unknown) =>
2907
+ bestEffort(err, `terminateChatSession (${kind}, ${source})`, "debug"),
2908
+ );
2909
+ }
2910
+
2911
+ /** chat record 的协议 interact handle(engineHandle 缺省时按 recordId 合成——
2912
+ * 引擎侧 interact 定位键 = sessionRef.recordId;sessionFile 有值时一并携带)。 */
2913
+ private chatHandleFor(record: ExecutionRecord): EngineHandle {
2914
+ const known = record.engineHandle;
2915
+ return {
2916
+ data: {
2917
+ v: 1,
2918
+ engineId: record.engine ?? DEFAULT_ENGINE_ID,
2919
+ sessionRef: {
2920
+ recordId: record.id,
2921
+ ...(known?.sessionRef["sessionFile"] !== undefined
2922
+ ? { sessionFile: known.sessionRef["sessionFile"] }
2923
+ : record.sessionFile !== undefined
2924
+ ? { sessionFile: record.sessionFile }
2925
+ : {}),
2926
+ },
2927
+ poolKey: known?.poolKey ?? PI_POOL_KEY,
2928
+ ...(known?.journalPath !== undefined ? { journalPath: known.journalPath } : {}),
2929
+ adapterVersion: "subagent-core/host-bridge",
2930
+ },
2931
+ };
2932
+ }
2933
+
2934
+ /** [W3] pi 引擎 port 解析(chat 域路由与终止面共用):registry cli 形态 port,
2935
+ * 未注册 = 不可用 stub(engine_not_found,见 pi-host-binding)。 */
2936
+ private resolveChatEnginePort(): EnginePort {
2937
+ return resolveHostPiEnginePort(() => null);
2938
+ }
2939
+
2940
+ /** [W3] record 终态化路径的 chat 反向通道路由注销(幂等)。 */
2941
+ private unregisterChatRoundRoute(recordId: string): void {
2942
+ const unregister = this.chatRoundRoutes.get(recordId);
2943
+ if (unregister !== undefined) {
2944
+ this.chatRoundRoutes.delete(recordId);
2945
+ unregister();
2946
+ }
2947
+ }
2948
+
2949
+ /** 引擎服务面适配器(HostBridgeServiceFace 结构视图):闭包持有本实例的编排面。
2950
+ * [W3] chat 域轮次交接可选面(takeChatRound/runChatRound/resumeChatRound)随
2951
+ * inproc PiEngine 删除移除——引擎经协议 converse,不再回调宿主编排面。 */
2952
+ private piEngineServiceAdapter(): HostBridgeServiceFace {
2149
2953
  return {
2150
2954
  executeAndAwait: (opts, signal, onEvent, stream) => this.executeAndAwait(opts, signal, onEvent, stream),
2151
2955
  getRecordForAction: (id) => this.getRecordForAction(id),
2152
2956
  closeSubagent: (record, force) => this.closeSubagent(record, force),
2153
2957
  cancel: (id) => this.cancel(id),
2154
2958
  collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
2155
- takeChatRound: (taskId) => this.takeChatTicket(taskId),
2156
- runChatRound: (ticket) => this.runTicketRound(ticket),
2157
- resumeChatRound: (record, text) => this.resumeColdRound(record, text),
2158
2959
  reportRecordTransition: (record) => this.store.reportRecordTransition(record),
2159
2960
  };
2160
2961
  }
2161
2962
 
2162
- /** [D4 聚合连带] PiEngineService 的显式结构视图(registration / SAR 直绑消费)。
2163
- * 原两绑定点把 SubagentService 整体结构化兼容为 PiEngineService——依赖查询/交互
2164
- * 方法 public;聚合收窄后改经本视图显式适配(成员集合与原直绑等价,行为零差异)。
2165
- * getter 形态:face 视图(惰性构造的适配对象)而非动作方法。 */
2166
- get asEngineService(): PiEngineService {
2963
+ /** [D4 聚合连带] 引擎服务面的显式结构视图(SAR 构造 resolveHostPiEnginePort
2964
+ * 传入的 getService 兼容位消费——W3 后该面无引擎侧消费方,保留使 SAR 调用点
2965
+ * 零改动,W8 收口时随签名一并清理)。getter 形态:face 视图(惰性构造)。 */
2966
+ get asEngineService(): HostBridgeServiceFace {
2167
2967
  return this.piEngineServiceAdapter();
2168
2968
  }
2169
2969
 
2170
- /** chat 域轮次交接的消费侧(PiEngine.run 回调):一次性取走,防重复消费。私有名与
2171
- * PiEngineService 可选面成员不同名(历史约束:D4 聚合前的结构化直绑时代,私有同名
2172
- * 成员会阻断 SubagentService 直绑;聚合后经 asEngineService 显式视图,约束已消失,
2173
- * 不同名保留为与 adapter 显式映射一致的命名纪律)。 */
2174
- private takeChatTicket(taskId: string): ChatRoundTicket | undefined {
2175
- const ticket = this.chatRoundTickets.get(taskId);
2176
- if (ticket !== undefined) this.chatRoundTickets.delete(taskId);
2177
- return ticket;
2178
- }
2179
-
2180
- /** PiEngine chat 分支的执行回调:原 runAndFinalize 直调链的 EnginePort 化落点
2181
- * (编排归 Service——pool 槽 + runSpawn + 终态迁移,行为零变化)。 */
2182
- private async runTicketRound(ticket: ChatRoundTicket): Promise<AgentResult> {
2183
- return this.runAndFinalize(
2184
- ticket.record,
2185
- ticket.opts,
2186
- ticket.ctx,
2187
- ticket.identity,
2188
- ticket.signal,
2189
- ticket.priority,
2190
- undefined,
2191
- ticket.stream,
2192
- ticket.resume,
2193
- );
2194
- }
2195
-
2196
2970
  /**
2197
2971
  * 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
2198
2972
  * 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
@@ -2215,9 +2989,17 @@ export class SubagentService {
2215
2989
  // [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
2216
2990
  // 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
2217
2991
  // 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
2992
+ // [W3] chatMode 的实际终止经协议 interact cancel(引擎进程内 SIGTERM → settle
2993
+ // 等待 → 杀链升级;首轮在途 run 的 abort 帧由 controller.signal 经 RemoteEngine
2994
+ // cancel 分级另行承载,两路幂等)。
2218
2995
  killRecordChildWithEscalation(record.id, "cancelBackground");
2219
2996
  disarmIdleTimer(record.id);
2220
2997
  disarmSettledWatchdog(record.id);
2998
+ disarmRoundFromProtocol(record.id);
2999
+ this.unregisterChatRoundRoute(record.id);
3000
+ if (record.chatMode) {
3001
+ this.terminateChatSession(record, "cancel", "cancelBackground");
3002
+ }
2221
3003
  // [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
2222
3004
  // record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
2223
3005
  // cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
@@ -2258,7 +3040,7 @@ export class SubagentService {
2258
3040
  // pending-notifications:cancel 注销(只记 registry 状态)
2259
3041
  this.notifyHost.emitPendingUnregister(record.id, "closed");
2260
3042
  // cancel 完成通知(与轮次收尾 .then 对称——cancel 抢先时 .then 跳过 notify)
2261
- this.notifyHost.notifyComplete(record);
3043
+ this.collectCoordinator.route(record);
2262
3044
  return true;
2263
3045
  }
2264
3046
 
@@ -2279,6 +3061,10 @@ export class SubagentService {
2279
3061
  modelService: this.modelService,
2280
3062
  pi: this.pi,
2281
3063
  emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
3064
+ // [F-5 修复] chat record 终态化的路由注销(单一汇聚点钩子,见 FinalizeDeps
3065
+ // .onFinalized)——chatRoundRoutes 仅 chatMode 注册且注销幂等,非 chat record
3066
+ // 经此为 no-op。
3067
+ onFinalized: (id) => this.unregisterChatRoundRoute(id),
2282
3068
  sessionDir: this.sessionsDir,
2283
3069
  },
2284
3070
  record,
@@ -2312,7 +3098,13 @@ export class SubagentService {
2312
3098
 
2313
3099
  /** run() 创建期异常的收尾(H1 修复):createAndConfigureSession 失败会抛,本方法合成 failed
2314
3100
  * AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
2315
- * 继续返回(不 re-throw,swallow 策略)。 */
3101
+ * 继续返回(不 re-throw,swallow 策略)。
3102
+ * [W3 契约变更⑤(run 期失败清理前置副作用)] kickOffEngineRun 前已建的 worktree
3103
+ * (executeViaEngine 创建点,record.worktreeHandle 已绑定)经本方法 → finalizeRecord
3104
+ * → doFinalizeRecord Step 3b cleanupWorktreeIfBound 清理(manifest 多声明 run 期
3105
+ * 失败 / engine.run prepare 期 reject 共用本收尾链);CAS 没抢锁(cancel 抢先终态)
3106
+ * 时由 cancelBackground 的 worktree cleanup 覆盖。唯一前置副作用 = worktree(并发
3107
+ * 池槽 acquire/release 在 kickOffEngineRun finally 内自回收,journal 是宿主数据不清理)。 */
2316
3108
  private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
2317
3109
  const errMsg = toErrorMessage(err);
2318
3110
  // durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
@@ -2387,43 +3179,6 @@ export class SubagentService {
2387
3179
  );
2388
3180
  }
2389
3181
  }
2390
-
2391
- /** 构造 SessionRunnerContext(spawn 模式:无需 SDK 实例)。 */
2392
- private buildSessionRunnerContext(overrideCwd?: string): SessionRunnerContext {
2393
- return {
2394
- cwd: overrideCwd ?? this.cwd,
2395
- agentDir: this.modelService.getAgentDir(),
2396
- // ADR-031 废弃 discovery.json 后,skillDirs 为空。子 session 的 --skill
2397
- // 由 agent({skill}) 调用方显式传入(resolveSkillPath → opts.skillPath)。
2398
- skillDirs: [],
2399
- mainCwd: this.cwd,
2400
- // mainSessionFile: fork source 解析用,从 session_start 缓存获取。
2401
- mainSessionFile: this.getMainSessionFile?.() ?? undefined,
2402
- // worktree pid 回调:session-runner first header 时补全注册表 pid。
2403
- onWorktreePid: (branch: string, pid: number, sessionFile?: string) => this.worktreeManager.registerPid(branch, pid, sessionFile),
2404
- uiRequestHandler: this.uiRequestHandler,
2405
- // SR-4:L2 dialog 队列透传——child close 时 session-runner 据此调 rejectChildDialogs
2406
- // 清理 L2 pending dialog,防全局死锁。undefined 时 session-runner 跳过 L2 清理。
2407
- dialogQueue: this.dialogQueue,
2408
- // 主进程运行模式:session-runner W4 守卫据此决定是否注入 ask_user RPC 提示词。
2409
- mode: this.uiObservability.getMode(),
2410
- // [递归可见性] 透传所属根 session(runSpawn 注入为子进程 env PI_SUBAGENT_ROOT_SESSION_ID)。
2411
- // sessionRootId 在 initSession 设定(根进程=sessionId,子进程=env 贯穿的真 ROOT)。
2412
- // execute/executeAndAwait 调本方法前必经 initSession,此时 sessionRootId 已非空;
2413
- // ?? 兑底防类型漂移(运行时不可达)。
2414
- sessionRootId: this.sessionRootId ?? this.sessionId ?? "",
2415
- // [MF-3] 透传 ROOT cwd(runSpawn 落盘目录编码键 + 注入子进程 env PI_SUBAGENT_ROOT_CWD)。
2416
- // worktree 模式下 mainCwd = 本进程 checkout 路径,rootCwd 才是真 ROOT——session 文件
2417
- // 落盘统一用 rootCwd 编码,ROOT 磁盘重建才扫得到深层 record(与 sessionRootId 同构)。
2418
- rootCwd: this.rootCwd,
2419
- // [V2 决策 2] chatMode 首轮闭环:agent_settled 时 session-runner 调本回调。
2420
- // [D4-②] 业务闭包已拆至 round-settlement.ts(this.settleRound 字段工厂构造,
2421
- // deps 回调注入 notifyComplete / reportRecordTransition / closeAfterRoundSettled,
2422
- // 行为逐字节等价)——轻量 idle 化语义(round+=1 + notify 主 agent,不调
2423
- // doFinalizeRoundToIdle)与增量派生/base 推进/哨兵的机制注释见该文件。
2424
- onRoundSettled: this.settleRound,
2425
- };
2426
- }
2427
3182
  }
2428
3183
 
2429
3184
  // ── 进程单例访问器 ────────────────────────────────────