@zhushanwen/subagent-core 0.6.0 → 0.7.1

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 (278) 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-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -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 +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  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__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. 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
 
@@ -18,11 +18,12 @@ import { removeAliveMarker } from "./alive-store.ts";
18
18
  import { bestEffort } from "./best-effort.ts";
19
19
  import { CollectCoordinator } from "./collect-coordinator.ts";
20
20
  import { DEFAULT_COLLECT_SYNC } from "./config.ts";
21
- // [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程)。
21
+ // [V2 决策 3] lifecycle-manager idle timer:chatMode 统一投递新 turn disarm(防误杀活进程);
22
+ // idle 相位帧(协议 host/roundLifecycle)= arm 锚点(W3 v1.x——引擎不实现 idle 定时器)。
22
23
  // [T4②] DEFAULT_IDLE_TIMEOUT_MS:assertIdleTimeoutMsSafe 错误文案的缺省时长基准。
23
24
  // ([M3] hasIdleTimer / hasLiveProcessHandle 的 piAdapter.hasRunningBackground 过滤随通知簇
24
25
  // 在 notify-host.ts 消费,不经本文件。)
25
- import { disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
26
+ import { armIdleTimer, disarmIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
26
27
  import { type ConcurrencyPool,DefaultConcurrencyPool } from "./concurrency-pool.ts";
27
28
  import type { DialogGlobalQueue, UiRequestHandler } from "./dialog-queue.ts";
28
29
  import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "./cold-resurrect.ts";
@@ -38,14 +39,23 @@ import { assertTaskShapeSupported } from "./engine/common/capability-gate.ts";
38
39
  import { ExecutionNestingContext } from "./engine/common/nesting-guard.ts";
39
40
  import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "./engine/common/journal-wiring.ts";
40
41
  import { executeOptionsToEngineTaskSpec } from "./engine/host-task-spec.ts";
41
- import { PiEngine } from "./engine/engines/pi/pi-engine.ts";
42
- import { PI_POOL_KEY } from "./engine/engines/pi/pi-engine.ts";
43
- 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";
44
49
  import type { EnginePort, RunContext } from "./engine/port.ts";
45
50
  import { DEFAULT_ENGINE_ID, getEngine, listEngines } from "./engine/registry.ts";
46
- import { validateModelForEngine, withCrossEngineHint } from "./engine/model-validation.ts";
51
+ import {
52
+ joinEngineModelRef,
53
+ splitEngineModelRef,
54
+ validateModelForEngine,
55
+ withCrossEngineHint,
56
+ } from "./engine/model-validation.ts";
47
57
  import { type EngineRouteResult, routeEngineForHost } from "./engine/routing.ts";
48
- import type { AgentOutcome } from "./engine/types.ts";
58
+ import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
49
59
  import { ManifestStore } from "./manifest-store.ts";
50
60
  import type { ModelConfigService } from "./model-config-service.ts";
51
61
  import type { AgentConfig, ModelInfo, ResolvedModel } from "./model-resolver.ts";
@@ -54,32 +64,47 @@ import { type NotifyHost, type PiLike, createNotifyHost } from "./notify-host.ts
54
64
  import type { BatchBudgetParams, BgNotifyRecord } from "./notifier.ts";
55
65
  import { getBoundNotifyLedger, NOTIFY_LEDGER_CUSTOM_TYPE } from "./notify-ledger.ts";
56
66
  import { getSubagentRecordsDir, getSubagentSessionDir } from "./path-encoding.ts";
57
- import { createRoundSettler } from "./round-settlement.ts";
58
67
  import type { StatusFilter } from "./record-store.ts";
59
68
  import { RecordStore } from "./record-store.ts";
60
69
  // [E1 恢复批语义修复 / S11] 恢复批补发映射 + getFullRecord miss 成员兜底落标数据源
61
70
  //(变化轴拆分,实现与语义注释见 sync-rebuild.ts)
62
71
  import { bufferedMemberFallbackRecord, syncRebuildToNotifyMember } from "./sync-rebuild.ts";
63
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。
64
76
  import {
65
77
  killAllSpawnedChildren,
66
78
  killRecordChildWithEscalation,
67
79
  registerSpawnedChildForRecord,
68
- runSpawn,
69
- type SessionRunnerContext,
70
- type SpawnResumeOpts,
71
- } from "./engine/engines/pi/session-runner.ts";
72
- // [u-t2a T2②/T2③] settled watchdog:chatMode 轮 settled 等待固定硬上限(10min),
73
- // 双挂载点之一在编排层热路径(deliverChatMessage,prompt 发出后),disarm 站点散布
74
- // 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 尾段)。
75
84
  import {
76
85
  armMidRoundNoProgress,
86
+ disarmRoundFromProtocol,
77
87
  disarmSettledWatchdog,
88
+ noteRoundSettledFromProtocol,
89
+ refreshFromProtocolEvent,
78
90
  type SettledWatchdogFireInfo,
79
91
  } from "./settled-watchdog.ts";
80
92
  import { isIdle, isResumable } from "./lifecycle-predicates.ts";
81
93
  import { startIdleGc } from "./idle-gc.ts";
82
- 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";
83
108
  import type { StreamSink, SubagentStream } from "./stream-sink.ts";
84
109
  import { createBackgroundStream } from "./stream-sink.ts";
85
110
  import { writeCancelledTombstone } from "./tombstone-store.ts";
@@ -142,13 +167,13 @@ export interface SubagentChatActions {
142
167
  * 子进程彻底 close 前(pi 子进程 trap SIGTERM 做 graceful shutdown,窗口几十~几百 ms),
143
168
  * 子进程的 trailing extension_ui_request 仍可能被父进程 pump 解析,调到 A 的 handler 闭包。
144
169
  * 若 dispose 不清 uiRequestHandler,旧 handler 闭包仍持有 A 的 ctx,触发
145
- * ui-request-queue.ts 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
170
+ * inproc UI 请求队列(已删) 的 catch 分支打 `[subagents] uiRequestHandler threw` 误导性
146
171
  * logger.error(看起来像 bug,实际是预期竞态;三层兜底已确保功能正确)。
147
172
  *
148
173
  * stub 始终返回 {cancelled:true},不调 ctx.ui、不捕获任何 ctx,让 trailing ui_request
149
174
  * 干净降级为 cancelled(等价于子进程主动取消)。
150
175
  *
151
- * 不置 undefined —— 那会让 trailing ui_request 走 ui-request-queue.ts 的 handler-missing
176
+ * 不置 undefined —— 那会让 trailing ui_request 走 inproc UI 请求队列(已删) 的 handler-missing
152
177
  * 分支触发 notifyMissingHandlerGlobal warn,噪声性质从 threw-error 变 missing-handler,
153
178
  * 没真正解决。 */
154
179
  const disposedUiRequestStub: UiRequestHandler = () => Promise.resolve({ cancelled: true });
@@ -370,22 +395,15 @@ export class SubagentService {
370
395
  * 兜底不持锁,同样被覆盖)。child 注册完成后投递走热路径,不经此守卫。 */
371
396
  private readonly resumesInFlight = new Set<string>();
372
397
 
373
- /** chat 域 pi 引擎实例(D2 单轨:chat 域执行/投递统一经 EnginePort)。per-service DI——
374
- * getService 经适配器绑本实例;registry 全局 'pi' 单例绑进程级 getSubagentService(),
375
- * 直构 Service 的测试场景解析不到本实例。不能 import registration.ts(其 import 本文件
376
- * 循环依赖),直接构造 PiEnginepi-engine 不反向依赖本文件)。 */
377
- private readonly chatPiEngine: PiEngine = new PiEngine({ getService: () => this.piEngineServiceAdapter() });
378
-
379
- /** chat 域轮次交接包(executeViaEngine / 冷路径续轮挂载 → PiEngine.run 经 taskId 消费)。 */
380
- private readonly chatRoundTickets = new Map<string, ChatRoundTicket>();
381
-
382
- /** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
383
- * 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
384
- private readonly settleRound = createRoundSettler({
385
- notifyComplete: (record) => this.collectCoordinator.route(record),
386
- reportRecordTransition: (record) => this.store.reportRecordTransition(record),
387
- closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record),
388
- });
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>();
389
407
 
390
408
  /** [D4-③] 冷路径复活依赖(原四件 private 方法的搬移落点——cold-resurrect.ts;
391
409
  * deps 闭包惰性求值:sessionRootId / execNesting 基线运行时可变)。 */
@@ -399,6 +417,21 @@ export class SubagentService {
399
417
  getBaselineRecordId: () => this.execNesting.baseline()?.recordId ?? undefined,
400
418
  };
401
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
+
402
435
  /** [D4 查询面聚合] 读模型消费面(壳 interface/ 视图与 tool 查询经此访问;
403
436
  * 纯委托——方法本体保留 private 实现不重写,行为逐字节等价)。 */
404
437
  readonly queries: SubagentQueries = {
@@ -437,6 +470,9 @@ export class SubagentService {
437
470
  this.modelService = init.modelService;
438
471
  this.getMainSessionFile = init.getMainSessionFile;
439
472
  this.uiRequestHandler = init.uiRequestHandler;
473
+ // [W6 R3 MF-A] 壳侧应答端登记:cli 形态引擎经 host/askUser 反向请求消费(discovery
474
+ // portFactory 构造 EngineClient 时读取该登记)。
475
+ setHostUiRequestEndpoint(init.uiRequestHandler);
440
476
  this.pool = new DefaultConcurrencyPool(this.modelService.getGlobalConfig().maxConcurrent);
441
477
  this.worktreeManager = new WorktreeManager(this.modelService.getAgentDir());
442
478
  // [MF-3] worktree 隔离下全树落盘目录统一到 ROOT cwd:子进程(spawn cwd = worktree checkout 路径)
@@ -526,7 +562,7 @@ export class SubagentService {
526
562
  }
527
563
  },
528
564
  });
529
- // #11:注册进程级 observability 单例——ui-request-queue.handleUiRequest 经
565
+ // #11:注册进程级 observability 单例——inproc UI 请求队列 handleUiRequest(已删)
530
566
  // globalThis 桥接(notifyMissingHandlerGlobal)调到同一实例,共享
531
567
  // warnedMissingHandlerSessions 去重集合。未注册时 queue 走 fallback warn(不去重)。
532
568
  registerGlobalObservability(this.uiObservability);
@@ -553,6 +589,8 @@ export class SubagentService {
553
589
  if (init.uiRequestHandler !== undefined) {
554
590
  this.uiRequestHandler = init.uiRequestHandler ?? undefined;
555
591
  this.uiObservability.resetMissingHandlerWarnings();
592
+ // [W6 R3 MF-A] session 级覆盖同步进壳侧应答端登记(三态:null = 显式清空)。
593
+ setHostUiRequestEndpoint(this.uiRequestHandler);
556
594
  }
557
595
  // SR-4:注入 L2 dialog 队列(child close 清理路径)。undefined 时 buildSessionRunnerContext
558
596
  // 透传 undefined,session-runner onClose 跳过 L2 清理(仅清 L1,保留旧行为)。
@@ -581,6 +619,21 @@ export class SubagentService {
581
619
  // 孤儿终态恢复(放 initSession 末尾:setPi 已注入(appendEntry 可用)、
582
620
  // sessionRootId 已建立(过滤当前根的 record);单扫描者判据见 recoverOrphansIfRootProcess)
583
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
+ );
584
637
  }
585
638
 
586
639
  /**
@@ -730,6 +783,9 @@ export class SubagentService {
730
783
  };
731
784
  completeRecord(record, result, "closed", reason);
732
785
  this.store.archive(record);
786
+ // [F-5 修复] 本路径不经 doFinalizeRecord(编排性关闭直连 completeRecord+archive),
787
+ // chat 轮路由注销在此补齐(幂等;闭包持 record/stream 引用,防泄漏)。
788
+ this.unregisterChatRoundRoute(record.id);
733
789
  // worktree 绑定清理(T3)。cleanup 已 async 化——同步签名(返回计数)不变,
734
790
  // 清理 fire-and-forget:失败经 bestEffort 留痕,不阻塞/不影响计数返回。
735
791
  if (record.worktreeHandle) {
@@ -760,10 +816,16 @@ export class SubagentService {
760
816
  /** SP-4: idle record GC(30 天 TTL,实现抽至 idle-gc.ts)。stop 函数(dispose 调)。 */
761
817
  private stopIdleGc: (() => void) | undefined;
762
818
 
763
- /** 启动 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 引入的装配错位)。 */
764
826
  startGcTimer(): void {
765
827
  if (this.stopIdleGc) return;
766
- this.stopIdleGc = startIdleGc(this.store);
828
+ this.stopIdleGc = startIdleGc(this.store, new FileRunStore({ stateDir: resolvePiWorkflowStateDir() }));
767
829
  }
768
830
 
769
831
  /** 停止 idle record GC 定时器(dispose 调用)。 */
@@ -1050,10 +1112,17 @@ export class SubagentService {
1050
1112
  // 此处内联其方法体(赋值 + 缺失告警去重重置)。
1051
1113
  this.uiRequestHandler = disposedUiRequestStub;
1052
1114
  this.uiObservability.resetMissingHandlerWarnings();
1115
+ // [W6 R3 MF-A] dispose 后壳侧应答端同步换 stub(trailing host/askUser 干净降级为
1116
+ // cancelled,与 inproc inproc UI 请求队列(已删) 的 trailing 语义同构)。
1117
+ setHostUiRequestEndpoint(disposedUiRequestStub);
1053
1118
  // [R0/C1 孤儿进程修复] 先 abort running controllers + kill spawned children,再 dispose 资源。
1054
1119
  // abortRunningControllers 需要在 disposeAllRecords archive 之前执行(archive 后 store 找不到 record)。
1055
1120
  this.store.abortRunningControllers();
1056
1121
  killAllSpawnedChildren();
1122
+ // [W3] chat 反向通道路由全量注销(inproc 轮次交接包清空的协议形态替位)。
1123
+ for (const recordId of [...this.chatRoundRoutes.keys()]) {
1124
+ this.unregisterChatRoundRoute(recordId);
1125
+ }
1057
1126
  // [E9] 批未闭合时缓冲终态成员逐条转 async 写账 + 落 batchFinalized(设计 §3.1.5 E9)。
1058
1127
  // 必须在 disposeAllRecords 之前——它会把活跃 record(含 SP-5 成功回退的
1059
1128
  // running+resumable 缓冲成员)全部 archive 清内存,之后再 getFullRecord 落标只剩
@@ -1065,13 +1134,9 @@ export class SubagentService {
1065
1134
  // SP-4: 级联关闭所有活跃 record(parent-shutdown reason)
1066
1135
  // 在 abort/kill 之后执行:先终止子进程,再清理 record 状态。
1067
1136
  this.disposeAllRecords("parent-shutdown");
1068
- // [v4 A-1] EPIPE 连续失败计数器清零(计数器已迁移到 stdin-writer,防跨 session 泄漏)
1069
- resetAllEpipeFailures();
1070
1137
  // [review MF1] 在途 resume 守卫清空(正常由轮次收尾 finally 清除;此处兜底
1071
1138
  // abort/kill 后仍挂着的条目,防跨 session 复活时残留)
1072
1139
  this.resumesInFlight.clear();
1073
- // chat 轮次交接包清空(正常由 PiEngine.run 消费;此处兜底 kill 后仍挂着的条目)
1074
- this.chatRoundTickets.clear();
1075
1140
  // flush 待发通知(session_shutdown 尽力投递一次)。
1076
1141
  this.notifyHost.flushPendingNotifications();
1077
1142
  // [T4④ / PS-5] flush 被 isIdle 门拦时的丢失面闭合:attemptDeliver 的 isIdle 二次
@@ -1081,6 +1146,9 @@ export class SubagentService {
1081
1146
  // 账面差集重放补投——「不丢」由落盘账本承接而非本次 flush。
1082
1147
  this.persistUndeliveredNotificationsForReplay();
1083
1148
  this.notifyHost.dispose();
1149
+ // [W4] 监督器停机(清 timer + 纳管记账;注册/record 不动——process 档跨 shutdown
1150
+ // 存活,重开 session 由 boot 分区重认领 + sweep 对账收口)。
1151
+ this.roundSupervisor.dispose();
1084
1152
  this.store.dispose();
1085
1153
  }
1086
1154
 
@@ -1179,7 +1247,9 @@ export class SubagentService {
1179
1247
  // 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
1180
1248
  // 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
1181
1249
  // 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
1182
- // record。pi 请求路径同步短路(routed 非 Promise,零微任务——缺省路径时序不变)。
1250
+ // record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
1251
+ // 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
1252
+ // 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
1183
1253
  // [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
1184
1254
  // engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
1185
1255
  // 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
@@ -1195,7 +1265,9 @@ export class SubagentService {
1195
1265
  taskModel: opts.model,
1196
1266
  strict: this.modelService.getGlobalConfig().engineRouting?.strict === true,
1197
1267
  probe: (engineId) => getEngine(engineId).probe(),
1198
- piEngine: this.chatPiEngine,
1268
+ // [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
1269
+ // 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
1270
+ piEngine: this.resolveChatEnginePort(),
1199
1271
  });
1200
1272
  const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
1201
1273
  return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
@@ -1255,42 +1327,74 @@ export class SubagentService {
1255
1327
  // deliverChatMessage → PiEngine.interactRecord 调用,见下方两方法。
1256
1328
 
1257
1329
  /**
1258
- * [V2 决策 3] chatMode 统一投递入口(message action 的 Service 面)——经引擎交互面
1259
- * 执行(D2:PiEngine.interactRecord——port face interact record 锚定形态,协议知识
1260
- * 在引擎边界,编排层不做 stdin 写入)。分流语义(按**进程死活**,
1261
- * 不按 record.status)与热/冷路径细节见 PiEngine.deliverPrompt:
1262
- *
1263
- * 热路径(进程活):prompt + streamingBehavior——pi 权威裁决 busy/idle(F3/F4)。
1264
- * 冷路径(进程死):冷路径续轮(resumeChatRound)重开 session + prompt(仅崩溃/
1265
- * timeout kill/跨重启命中)。
1330
+ * [V2 决策 3 → W3 协议形态] chatMode 统一投递入口(message action 的 Service 面)
1331
+ * ——经协议 interact(message action)发往 pi-subagent-cli 引擎进程。协议知识
1332
+ * stdin prompt 命令 + streamingBehavior 映射 + EPIPE 兜底)在引擎进程内
1333
+ * (chat-session.deliverMessage);编排层按结构化结果分流:
1266
1334
  *
1267
- * 失败语义与直调形态一致:业务拒绝(not ready / EPIPE 兜底耗尽等,文案自带行动语言)
1268
- * 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 形态逐字节一致)。
1269
1340
  *
1270
1341
  * @param record 目标 record(chatMode,running 或 idle)
1271
1342
  * @param text 消息正文
1272
- * @param interrupt true=steer(抢占)/ false=followUp(排队),仅热路径 streamingBehavior 用
1343
+ * @param interrupt true=steer(抢占)/ false=followUp(排队)
1273
1344
  */
1274
1345
  private async deliverChatMessage(record: ExecutionRecord, text: string, interrupt: boolean): Promise<void> {
1275
1346
  this.assertReady();
1276
- // interactRecord:interact record 锚定形态(调用方已持归属校验过的同一 record
1277
- // 对象——port face interact 的 handle 解析在此冗余且会做二次 store 查找)
1278
- const result = await this.chatPiEngine.interactRecord(
1279
- record,
1280
- { kind: "message", payload: text, interrupt },
1281
- );
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
+ });
1282
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
+ }
1283
1383
  throw new Error(result.message);
1284
1384
  }
1285
- // [T2③ / D9 两段式] 热路径轮 settled 等待守护(双挂载原语之热路径调用点,首轮
1286
- // 调用点在 session-runner runSpawn)。原挂载位在 deliverMessage prompt 写入成功后
1287
- // 的同步段——D2 协议知识下沉 PiEngine.deliverPrompt 后编排层在 interact 返回点
1288
- // arm(deliverPrompt 热路径段无 await,返回点距 prompt 发出仅差微任务链;窗口口径
1289
- // 「整轮含 turn 执行与收尾」不受影响)。冷路径(EPIPE 兜底/resume)下 runSpawn 的
1290
- // 首轮 arm 与此处重复挂载由原语幂等(先清旧 timer)吸收 = 窗口重置,无害。
1291
- // prompt 发出后挂**中段**无进展检测(有效协议事件行刷新,连续静默判 wedged)+
1292
- // agent_end 交棒收尾段固定上界;settled 到达(PiEngine.handleSdkEvent disarm)/
1293
- // 进程 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 终态化即清。
1294
1398
  armMidRoundNoProgress(record.id, {
1295
1399
  onMidTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
1296
1400
  onSettleTimeout: (fire) => this.onHotPathSettledWatchdogTimeout(record, fire),
@@ -1385,24 +1489,28 @@ export class SubagentService {
1385
1489
  // W16 [D4]:冷路径续轮是类外状态写点(不走 register/archive),显式上报迁移。
1386
1490
  this.store.reportRecordTransition(record);
1387
1491
 
1388
- // resume 参数从 record identity 读(防漂移,P-10)。
1389
- const resume: SpawnResumeOpts = {
1390
- sessionFile: record.sessionFile,
1391
- model: record.model,
1392
- 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,
1393
1502
  };
1394
1503
 
1395
- // 重建 resolved:runSpawn resume.model/thinkingLevel 优先(覆盖 resolved),
1396
- // resolved.model.id 仅在 runSpawn 内被读(resume 短路时不报错)。从 record.model
1397
- // (createRecordForMode 写入的 "provider/id" 格式)解析 provider/id 构造最小 ModelInfo。
1398
- const slashIdx = record.model.indexOf("/");
1399
- const provider = slashIdx >= 0 ? record.model.slice(0, slashIdx) : "unknown";
1400
- 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);
1401
1509
  const identity: ResolvedIdentity = {
1402
1510
  agent: record.agent,
1403
1511
  agentConfig: undefined,
1404
1512
  resolved: {
1405
- model: { id: modelId, name: record.model, provider, reasoning: false },
1513
+ model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
1406
1514
  thinkingLevel: record.thinkingLevel,
1407
1515
  },
1408
1516
  };
@@ -1411,15 +1519,17 @@ export class SubagentService {
1411
1519
  task: text,
1412
1520
  slug: record.slug,
1413
1521
  worktree: record.worktreeHandle,
1522
+ // 冷续轮同样是 chat 会话形态(run.params.chat 必传 + conversation gate 放行)。
1523
+ conversation: true,
1414
1524
  };
1415
- const ctx = this.buildSessionRunnerContext();
1416
1525
 
1417
- // detached 编排:轮次在 background 跑,pool 重新 acquire(轮次间 idle release)。
1418
- // 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)。
1419
1529
  // [review MF1] 在途标记在 kickOff 前同步设置:本方法返回即生效,后续重入
1420
- // (冷路径 / EPIPE 兜底)在守卫处被拒;轮次收尾 finally 统一清除。
1530
+ // (冷路径重入)在守卫处被拒;轮次收尾 finally 统一清除。
1421
1531
  this.resumesInFlight.add(record.id);
1422
- this.kickOffChatRound(record, opts, identity, ctx, record.controller.signal, PRIORITY_BACKGROUND, resume);
1532
+ this.kickOffChatRound(record, opts, identity, record.controller.signal, PRIORITY_BACKGROUND, resume);
1423
1533
  }
1424
1534
 
1425
1535
  /**
@@ -1447,6 +1557,9 @@ export class SubagentService {
1447
1557
  `terminating (LC-1 wedge recovery)`,
1448
1558
  );
1449
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)");
1450
1563
  const failedResult: AgentResult = {
1451
1564
  text: "",
1452
1565
  turns: record.turnCount,
@@ -1474,7 +1587,7 @@ export class SubagentService {
1474
1587
  }
1475
1588
 
1476
1589
  // [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
1477
- // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/engines/pi/pi-engine.ts
1590
+ // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/inproc pi-engine(已删)
1478
1591
  // rearmIdleTimerAfterHotPathFailure,该文件归 engine 域)——编排层 interactRecord 的
1479
1592
  // 结构化结果无法区分「stdin 写失败」与业务拒绝,盲目 re-arm 会误武装。
1480
1593
 
@@ -1598,13 +1711,16 @@ export class SubagentService {
1598
1711
  */
1599
1712
  private async closeChatIdle(record: ExecutionRecord): Promise<void> {
1600
1713
  // [M5] Path A:回收保活进程 + disarm idle timer(终态化后无其他 kill 路径)。
1601
- // [T2④ / LC-2] SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
1602
- // 30s 升级 SIGKILL——终态化后 record 已 archive,此前裸 SIGTERM 挂住 = 幽灵进程
1603
- // 无任何后续回收通道。同时 disarm settled watchdog(本路径终态化 = settled 等待
1604
- // 窗口终结,防 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 只做镜像置死记账。
1605
1719
  disarmIdleTimer(record.id);
1606
1720
  disarmSettledWatchdog(record.id);
1721
+ disarmRoundFromProtocol(record.id);
1607
1722
  killRecordChildWithEscalation(record.id, "closeChatIdle");
1723
+ this.terminateChatSession(record, "close", "closeChatIdle");
1608
1724
  // 合成 closed result(无在途 AgentResult,对齐 closeAfterRoundSettled 的
1609
1725
  // `record.result ?? ""` 模式)。[W16 P-1 修复] text 必须沿用轮终真实 result:
1610
1726
  // completeRecord 会执行 record.result = result.text,合成空串会把轮终真实值抹空,
@@ -1626,6 +1742,8 @@ export class SubagentService {
1626
1742
  modelService: this.modelService,
1627
1743
  pi: this.pi,
1628
1744
  emitUnregister: (id, st) => this.notifyHost.emitPendingUnregister(id, st),
1745
+ // [F-5 修复] 同 finalizeRecord——chat 轮路由注销单一汇聚点钩子。
1746
+ onFinalized: (id) => this.unregisterChatRoundRoute(id),
1629
1747
  sessionDir: this.sessionsDir,
1630
1748
  },
1631
1749
  record,
@@ -1657,11 +1775,14 @@ export class SubagentService {
1657
1775
  */
1658
1776
  private async closeAfterRoundSettled(record: ExecutionRecord): Promise<void> {
1659
1777
  // 回收保活进程(Path A:轮次完成后进程仍活)+ disarm idle timer(终态化后无其他 kill 路径)。
1660
- // [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation(30s 升级 SIGKILL
1661
- // 终态化后无后续回收通道);settled 等待窗口同步终结(disarm 幂等)。
1778
+ // [T2④ / LC-2] 终止语义收敛(30s 升级 SIGKILL,终态化后无后续回收通道);
1779
+ // settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
1780
+ // force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
1662
1781
  disarmIdleTimer(record.id);
1663
1782
  disarmSettledWatchdog(record.id);
1783
+ disarmRoundFromProtocol(record.id);
1664
1784
  killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
1785
+ this.terminateChatSession(record, "close", "closeAfterRoundSettled");
1665
1786
  if (!tryTransition(record, "closed", "user-close")) {
1666
1787
  return; // 已被 cancel/finalize 抢先(CAS 失败),标志已消费即可——不发终态通知(幂等)
1667
1788
  }
@@ -1752,17 +1873,13 @@ export class SubagentService {
1752
1873
  }
1753
1874
  }
1754
1875
 
1755
- // ── 步骤 3: SessionRunnerContext ──
1756
- const ctx = this.buildSessionRunnerContext(opts.cwd);
1757
-
1758
- // ── 步骤 4: signal 决议 ──
1876
+ // ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
1759
1877
  const effectiveSignal = signal ?? record.controller?.signal;
1760
1878
 
1761
1879
  // 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
1762
1880
  const result = await this.runAndFinalize(
1763
1881
  record,
1764
1882
  { ...opts, worktree: worktreeHandle },
1765
- ctx,
1766
1883
  identity,
1767
1884
  effectiveSignal,
1768
1885
  PRIORITY_BACKGROUND,
@@ -1911,19 +2028,21 @@ export class SubagentService {
1911
2028
  opts: ExecuteOptions,
1912
2029
  ): ResolvedIdentity {
1913
2030
  const canonical = validateModelForEngine(engine, engineModel);
1914
- // record.model 留痕:canonical(引擎裁决全名,含短名→缺省 provider 归一化);
1915
- // 引擎未实现校验面且无显式 model 时为空串(记录形态退化,生产不可达——注册表
1916
- // 内非 pi 引擎均实现 validateModel;防御性拼接避免 throw 打断兜底语义)。
2031
+ // record.model 留痕:canonical(引擎裁决 ref,允许无斜杠形态——契约变更④,协议化
2032
+ // 后引擎可原样返回 ref);引擎未实现校验面且无显式 model 时为空串(记录形态退化,
2033
+ // 生产不可达——注册表内非 pi 引擎均实现 validateModel;防御性空串避免 throw 打断
2034
+ // 兜底语义)。拆分单一权威 = splitEngineModelRef(无斜杠 → provider=""/id=ref/
2035
+ // 整串进 name,不再落 "<ref>/" 畸形)。
1917
2036
  const modelStr = canonical ?? engineModel ?? "";
1918
- const slashIdx = modelStr.indexOf("/");
2037
+ const model = splitEngineModelRef(modelStr);
1919
2038
  return {
1920
2039
  agent,
1921
2040
  agentConfig,
1922
2041
  resolved: {
1923
2042
  model: {
1924
- id: slashIdx > 0 ? modelStr.slice(slashIdx + 1) : "",
1925
- name: modelStr,
1926
- provider: slashIdx > 0 ? modelStr.slice(0, slashIdx) : modelStr,
2043
+ id: model.id,
2044
+ name: model.name,
2045
+ provider: model.provider,
1927
2046
  reasoning: false,
1928
2047
  },
1929
2048
  thinkingLevel: opts.thinkingLevel ?? agentConfig?.thinkingLevel,
@@ -1953,7 +2072,10 @@ export class SubagentService {
1953
2072
 
1954
2073
  const record = createRecord(id, {
1955
2074
  agent: identity.agent,
1956
- 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),
1957
2079
  thinkingLevel: identity.resolved.thinkingLevel,
1958
2080
  mode,
1959
2081
  task: opts.task,
@@ -2007,6 +2129,12 @@ export class SubagentService {
2007
2129
  // 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
2008
2130
  // 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
2009
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 能力位段)。
2010
2138
  assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
2011
2139
 
2012
2140
  // identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
@@ -2077,13 +2205,13 @@ export class SubagentService {
2077
2205
  }
2078
2206
 
2079
2207
  if (isPiRoute) {
2080
- // pi:record 耦合执行(runSpawn 直驱 record 记账)——预备轮次经 EnginePort 交接
2081
- //(kickOffChatRound),编排收尾(notify/终态迁移)与旧 pi 主路径语义一致。
2208
+ // pi:chat 轮次经协议 run(会话形态 chat{recordId})发往 pi-subagent-cli 引擎
2209
+ // 进程(W3——与 run 域 remote-engine 路由同构),编排收尾(notify/终态迁移)
2210
+ // 与旧 pi 主路径语义一致。
2082
2211
  this.kickOffChatRound(
2083
2212
  record,
2084
2213
  { ...recordOpts, worktree: worktreeHandle },
2085
2214
  identity,
2086
- this.buildSessionRunnerContext(opts.cwd),
2087
2215
  record.controller!.signal,
2088
2216
  PRIORITY_BACKGROUND,
2089
2217
  );
@@ -2104,6 +2232,8 @@ export class SubagentService {
2104
2232
  */
2105
2233
  private kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
2106
2234
  const signal = record.controller?.signal;
2235
+ // [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
2236
+ this.roundSupervisor.noteRunStarted(record.id);
2107
2237
  void (async () => {
2108
2238
  try {
2109
2239
  await this.pool.acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
@@ -2120,13 +2250,17 @@ export class SubagentService {
2120
2250
  // DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
2121
2251
  // maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
2122
2252
  try {
2123
- 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);
2124
2257
  // cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
2125
- if (record.closedReason !== "cancelled") {
2258
+ if (!adopted && record.closedReason !== "cancelled") {
2126
2259
  this.collectCoordinator.route(record);
2127
2260
  }
2128
2261
  } finally {
2129
2262
  this.pool.release();
2263
+ this.roundSupervisor.noteRunEnded(record.id);
2130
2264
  }
2131
2265
  })();
2132
2266
  }
@@ -2142,7 +2276,7 @@ export class SubagentService {
2142
2276
  opts: ExecuteOptions,
2143
2277
  engine: EnginePort,
2144
2278
  signal: AbortSignal | undefined,
2145
- ): Promise<void> {
2279
+ ): Promise<boolean> {
2146
2280
  // [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
2147
2281
  //(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
2148
2282
  // 不传 forwardEvents。
@@ -2193,15 +2327,49 @@ export class SubagentService {
2193
2327
  journalPath: journal.path,
2194
2328
  };
2195
2329
  await journal.close();
2196
- await this.finalizeEngineOutcome(record, outcome);
2330
+ return await this.finalizeEngineOutcome(record, outcome);
2197
2331
  } catch (err) {
2198
2332
  // engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
2199
2333
  // journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
2200
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
+ }
2201
2349
  await this.finalizeFailed(record, err);
2350
+ return false;
2202
2351
  }
2203
2352
  }
2204
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
+
2205
2373
  /**
2206
2374
  * 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
2207
2375
  * 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
@@ -2213,43 +2381,50 @@ export class SubagentService {
2213
2381
  /**
2214
2382
  * engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
2215
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 完成回注)。
2216
2391
  */
2217
- private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<void> {
2392
+ private async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
2218
2393
  if (outcome.sessionFile !== undefined) {
2219
2394
  record.sessionFile = outcome.sessionFile;
2220
2395
  }
2221
- const result: AgentResult = {
2222
- text: outcome.content,
2223
- turns: outcome.usage?.turns ?? 0,
2224
- durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
2225
- success: outcome.error === undefined,
2226
- ...(outcome.error !== undefined ? { error: outcome.error } : {}),
2227
- sessionId: outcome.sessionId ?? record.id,
2228
- toolCalls: [],
2229
- ...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
2230
- };
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);
2231
2406
  if (tryTransition(record, "closed", "gc")) {
2232
2407
  await this.finalizeRecord(record, result, "closed", "gc");
2233
2408
  }
2409
+ return false;
2234
2410
  }
2235
2411
 
2236
- // ── 执行内部:run + finalize(sync/bg 共用)──────────────
2412
+ // ── 执行内部:run + finalize(workflow 域 sync 面;chat 域轮次走 kickOffChatRound)──
2237
2413
 
2238
- /** 共享的"干活 + 收尾"——sync 直接 await,background 在 detached 里调。 */
2414
+ /**
2415
+ * workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
2416
+ * runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
2417
+ * 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
2418
+ */
2239
2419
  private async runAndFinalize(
2240
2420
  record: ExecutionRecord,
2241
2421
  opts: ExecuteOptions,
2242
- ctx: SessionRunnerContext,
2243
2422
  identity: ResolvedIdentity,
2244
2423
  signal: AbortSignal | undefined,
2245
2424
  priority: number,
2246
- rawOnEvent?: (event: AgentEvent) => void,
2425
+ onEvent?: (event: AgentEvent) => void,
2247
2426
  stream?: SubagentStream,
2248
- /** resume 选项(M2-B1):透传 runSpawn,重开已 idle 的 session 续聊。undefined = 新 session。 */
2249
- resume?: SpawnResumeOpts,
2250
2427
  ): Promise<AgentResult> {
2251
- // [U04 阶段化提取] 主函数只留编排,按「装配(池槽/worktree/深度)→ 执行(ALS 包装
2252
- // spawn)→ 回收(finally)→ 错误收口 → 终态收口」分段;行为逐字节不变。
2253
2428
  const pooled = record.mode === "background";
2254
2429
  let acquired = false;
2255
2430
  if (pooled) {
@@ -2257,48 +2432,106 @@ export class SubagentService {
2257
2432
  if (acquireFailure !== undefined) return acquireFailure;
2258
2433
  acquired = true;
2259
2434
  }
2260
- // onEvent 直通(原此处曾有 onUpdate(project(record)) 节流回流包装——生产死路径,
2261
- // 三调用点恒 onUpdate: undefined、仅测试触达,已按 swf-perf-impl ledger #22 删除
2262
- // (决策 TC4/IF14,详见 .cw/swf-perf-impl/cleanup-slice-design.json;git 历史可完整恢复)。
2263
- // ExecuteOptions.onUpdate 字段一并删除,未来误用将编译期失败而非静默无效。
2264
- const onEvent = rawOnEvent;
2265
2435
 
2266
- const worktreeHandle = this.resolveWorktreeHandle(opts);
2267
- 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 });
2268
2442
 
2269
2443
  let result: AgentResult;
2270
2444
  try {
2271
- result = await this.runSpawnNested(
2272
- record, opts, ctx, identity, signal, onEvent, stream, resume,
2273
- worktreeHandle, parentDepth, effectiveDepth,
2274
- );
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);
2275
2465
  } catch (err) {
2276
- // run() 正常路径不抛错,但创建期异常(createAndConfigureSession 失败)
2277
- // 会逃逸出 run() —— 合成 failed result + 收尾。
2278
- // swallow(不 re-throw):sync 调用方拿到合成 failed result,background 的
2279
- // .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。
2280
2470
  if (record.chatMode) return this.finalizeChatSpawnFailure(record, err);
2281
2471
  return this.finalizeFailed(record, err);
2282
2472
  } finally {
2283
2473
  this.releaseRoundResources(record, pooled && acquired, stream);
2284
2474
  }
2285
2475
 
2286
- // [V2 决策 2/3] chatMode 首轮闭环:runSpawn 因 agent_settled 提前 resolve(onRoundSettled
2287
- // 已设 record.status=idle + round+=1 + notify 主 agent)。进程仍保活(idle timer armed),
2288
- // 不进下方 chatMode 分流(那是 close 后 done/failed/cancelled 终态化的,走 finalizeRoundToIdle
2289
- // / finalizeRecord)。tryTransition(idle→done) 天然失败(要求 status==="running"),此处显式
2290
- // early return 让语义清晰 + 防状态机未来改动。防 double-notify 由 notifier dedup 兜底
2291
- //(同 id:round 60s 内吞,chat 轮次收尾 .then 的 notify 是 no-op,见 notifier.ts L122)。
2292
- if (record.chatMode && isIdle(record)) {
2293
- return result;
2294
- }
2295
-
2296
2476
  // v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
2297
- const aborted = signal?.aborted === true;
2298
- await this.settleFinalOutcome(record, result, aborted, this.deriveClosedReason(aborted, result.success));
2477
+ await this.settleOneShotOutcome(record, result, signal?.aborted === true);
2299
2478
  return result;
2300
2479
  }
2301
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
+
2302
2535
  /** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
2303
2536
  * 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
2304
2537
  * 不触发轮次资源回收——与原控制流逐字节一致)。 */
@@ -2322,69 +2555,16 @@ export class SubagentService {
2322
2555
  return typeof opts.worktree === "object" ? opts.worktree : undefined;
2323
2556
  }
2324
2557
 
2325
- /** [U04 提取·装配] [MF#4][MF#2] fork 深度护栏:ALS 传递深度(主 session 链无 store→0,fork 推进 +1)。 */
2326
- private resolveForkDepths(opts: ExecuteOptions): { parentDepth: number; effectiveDepth: number } {
2327
- const parentDepth = this.forkDepthAls.getStore() ?? this.forkDepthBaseline;
2328
- const effectiveDepth = opts.fork ? parentDepth + 1 : parentDepth;
2329
- return { parentDepth, effectiveDepth };
2330
- }
2331
-
2332
- /** [U04 提取·执行] runSpawn 的两层 ALS 包装(forkDepthAls 外层 + execNesting 内层)。 */
2333
- private runSpawnNested(
2334
- record: ExecutionRecord,
2335
- opts: ExecuteOptions,
2336
- ctx: SessionRunnerContext,
2337
- identity: ResolvedIdentity,
2338
- signal: AbortSignal | undefined,
2339
- onEvent: ((event: AgentEvent) => void) | undefined,
2340
- stream: SubagentStream | undefined,
2341
- resume: SpawnResumeOpts | undefined,
2342
- worktreeHandle: WorktreeHandle | undefined,
2343
- parentDepth: number,
2344
- effectiveDepth: number,
2345
- ): Promise<AgentResult> {
2346
- // 嵌套上下文包在 forkDepthAls 内层:B run() 期间挂 {recordId:B.id,depth:B.depth},
2347
- // B 内创建 C 时 createRecordForMode 读到 B → C 挂到 B 名下。两层 ALS 独立但同生命周期。
2348
- return this.forkDepthAls.run(effectiveDepth, () =>
2349
- this.execNesting.run(
2350
- { recordId: record.id, depth: record.depth },
2351
- () => runSpawn(record, opts.task, {
2352
- resolved: identity.resolved,
2353
- agentConfig: identity.agentConfig,
2354
- appendSystemPrompt: opts.appendSystemPrompt,
2355
- skillPath: opts.skillPath,
2356
- schema: opts.schema,
2357
- schemaEnv: opts.schemaEnv, // D-A6 bridge: workflow 编排层透传 schema 到 childEnv
2358
- maxTurns: opts.maxTurns,
2359
- graceTurns: opts.graceTurns,
2360
- signal,
2361
- onEvent,
2362
- stream, // text_delta streaming(background 路径有值,workflow 路径 undefined)
2363
- fork: opts.fork,
2364
- // [v8.5 B] fork-from 显式源(ExecuteOptions.forkFromSessionFile)优先于
2365
- // opts.fork 推导的 mainSessionFile;undefined = 旧语义不变。
2366
- forkSource: opts.forkFromSessionFile,
2367
- worktree: worktreeHandle,
2368
- parentForkDepth: parentDepth, // [MF#4] 父链深度,不从 opts 读
2369
- }, ctx, resume),
2370
- ),
2371
- );
2372
- }
2373
-
2374
2558
  /** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
2375
- * 成功)、streaming widget 清除、在途 resume 守卫清除。 */
2559
+ * 成功)、streaming widget 清除。 */
2376
2560
  private releaseRoundResources(
2377
- record: ExecutionRecord,
2561
+ _record: ExecutionRecord,
2378
2562
  holdSlot: boolean,
2379
2563
  stream: SubagentStream | undefined,
2380
2564
  ): void {
2381
2565
  if (holdSlot) this.pool.release();
2382
2566
  // 清除 streaming widget(subagent 终态,幂等)
2383
2567
  stream?.dispose();
2384
- // [review MF1] 清除在途 resume 守卫(幂等):本轮收尾——无论轮次完成(early return)、
2385
- // MF-6 失败回退 resumable、abort 还是终态化,record 都可再次接受冷路径 message。
2386
- // execute() 新建 record 不在集合,delete 是 no-op。
2387
- this.resumesInFlight.delete(record.id);
2388
2568
  }
2389
2569
 
2390
2570
  /** [U04 提取·错误收口] MF-6(决策 6 spec §3.1):chatMode(含 resume)spawn/创建失败
@@ -2401,6 +2581,7 @@ export class SubagentService {
2401
2581
  sessionId: record.id,
2402
2582
  toolCalls: [],
2403
2583
  };
2584
+ disarmRoundFromProtocol(record.id);
2404
2585
  if (tryTransition(record, "closed", "gc")) {
2405
2586
  // 回退 idle(record.result 由 finalizeRoundToIdle 设为 error 兜底文本,notify 可读)。
2406
2587
  await this.finalizeRoundToIdle(record, failedResult);
@@ -2408,81 +2589,6 @@ export class SubagentService {
2408
2589
  return failedResult;
2409
2590
  }
2410
2591
 
2411
- /** [U04 提取·收口派生] closedReason 派生:aborted → cancelled;否则 success → user-close,!success → gc。 */
2412
- private deriveClosedReason(aborted: boolean, success: boolean): ClosedReason {
2413
- return aborted ? "cancelled" : success ? "user-close" : "gc";
2414
- }
2415
-
2416
- /** [U04 提取·终态收口] CAS 抢锁:抢到则按 chatMode 分流完整收尾;没抢到(cancel 已先设
2417
- * closed+cancelled)则跳过。 */
2418
- private async settleFinalOutcome(
2419
- record: ExecutionRecord,
2420
- result: AgentResult,
2421
- aborted: boolean,
2422
- closedReason: ClosedReason,
2423
- ): Promise<void> {
2424
- if (!tryTransition(record, "closed", closedReason)) return;
2425
- if (record.chatMode) {
2426
- await this.settleChatRoundOutcome(record, result, aborted, closedReason);
2427
- } else {
2428
- await this.settleOneShotOutcome(record, result, aborted, closedReason);
2429
- }
2430
- }
2431
-
2432
- /** [U04 提取·终态收口] chatMode 轮次分流(原 A/B 分支,De Morgan 等价拆分):
2433
- * 成功轮(!aborted && success)与失败/取消轮(其补集 !success || aborted)各自处理
2434
- * closeAfterRound 挂起语义。 */
2435
- private async settleChatRoundOutcome(
2436
- record: ExecutionRecord,
2437
- result: AgentResult,
2438
- aborted: boolean,
2439
- closedReason: ClosedReason,
2440
- ): Promise<void> {
2441
- if (!aborted && result.success) {
2442
- if (record.closeAfterRound) {
2443
- // close 优雅关闭(force:false):当前轮完成后终态化为 closed。
2444
- await this.consumeCloseAfterRound(record, result, "user-close");
2445
- } else {
2446
- // 对话模式轮次成功完成 → 保持 running(旧 idle 折入 running,finalizeRoundToIdle 设回 running)。
2447
- await this.finalizeRoundToIdle(record, result);
2448
- }
2449
- } else {
2450
- // MF-6:chatMode 轮次失败/取消不销毁对话——回退 running-resumable(旧 idle,可恢复)。
2451
- if (record.closeAfterRound) {
2452
- // [M5] 优雅关闭挂起的失败/取消轮:轮已完成即兑现 close 意图终态化(含本轮 result),
2453
- // 不再回退 resumable——否则标志残留到下一轮,record 已被 tool 谎报 closed。
2454
- await this.consumeCloseAfterRound(record, result, closedReason);
2455
- } else {
2456
- await this.finalizeRoundToIdle(record, result);
2457
- }
2458
- }
2459
- }
2460
-
2461
- /** [U04 提取·终态收口] one-shot(非 chatMode)分流(原 C/D 分支):成功轮消费
2462
- * closeAfterRound 挂起标志;失败/取消一次性销毁。 */
2463
- private async settleOneShotOutcome(
2464
- record: ExecutionRecord,
2465
- result: AgentResult,
2466
- aborted: boolean,
2467
- closedReason: ClosedReason,
2468
- ): Promise<void> {
2469
- if (!aborted && result.success) {
2470
- if (record.closeAfterRound) {
2471
- // [M5] 非 chatMode(one-shot)busy 时 close(force:false) 置的标志在本轮完成时消费
2472
- // 终态化(对齐 close schema 文案 "release its resources")。旧代码走
2473
- // finalizeRoundToIdle 不消费——tool 返回 {closed:true} 谎报,record 永久
2474
- // running-resumable、5min idle timer 杀进程、期间还能继续收 message。
2475
- await this.consumeCloseAfterRound(record, result, "user-close");
2476
- } else {
2477
- // [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
2478
- await this.finalizeRoundToIdle(record, result);
2479
- }
2480
- } else {
2481
- // 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
2482
- await this.finalizeRecord(record, result, "closed", closedReason);
2483
- }
2484
- }
2485
-
2486
2592
  /** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
2487
2593
  * 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
2488
2594
  private async consumeCloseAfterRound(
@@ -2495,13 +2601,15 @@ export class SubagentService {
2495
2601
  }
2496
2602
 
2497
2603
  /**
2498
- * pi chat 域轮次的 detached 编排(D2 单轨——旧 pi 主路径绕过 EnginePort 的 detached
2499
- * 编排删除后的 EnginePort 化形态):
2500
- * 预备轮次(record/opts/identity/host ctx/stream/resume)挂载交接 Map engine.run
2501
- * ctx.taskId 消费(PiEngine chat 分支回调 runChatRound——即 runAndFinalize 链,含 pool
2502
- * 并发槽 acquire/release 与杀链,行为零变化)→ 完成回注 notify(cancel 抢先时跳过,
2503
- * 与旧 .then 语义一致)。
2504
- * 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 终态化路径注销。
2505
2613
  * chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源;与迁移前产物
2506
2614
  * 形态一致——journal 接线仅 workflow 域 SAR 与非 pi 引擎 chat 路径)。
2507
2615
  */
@@ -2509,26 +2617,105 @@ export class SubagentService {
2509
2617
  record: ExecutionRecord,
2510
2618
  opts: ExecuteOptions,
2511
2619
  identity: ResolvedIdentity,
2512
- ctx: SessionRunnerContext,
2513
2620
  signal: AbortSignal | undefined,
2514
2621
  priority: number,
2515
- /** resume 选项(M2-B1):透传轮次执行→runSpawn。undefined = 新 session。 */
2516
- resume?: SpawnResumeOpts,
2622
+ /** 冷续锚点(M2-B1 协议形态):run.params.chat.resume。undefined = 新 session。 */
2623
+ resume?: ResumeAnchor,
2517
2624
  ): void {
2518
2625
  // 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
2519
2626
  // 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
2520
- const stream = createBackgroundStream(record.id, this.streamSink, ctx.mode, process.env);
2521
-
2522
- this.chatRoundTickets.set(record.id, { record, opts, identity, ctx, signal, priority, stream, resume });
2523
- void this.chatPiEngine
2524
- .run(
2525
- // task 形参仅满足 port 签名——chat 轮次由 ticket lossless 携带
2526
- //(identity/forkFromSessionFile/host ctx host 编排件,不入任务声明
2527
- // AgentCallOpts——[u-3b D6] 双形态保留的结构性裁决见 ChatRoundTicket 注释)
2528
- executeOptionsToEngineTaskSpec(opts),
2529
- { taskId: record.id, poolKey: PI_POOL_KEY, signal, stream },
2530
- )
2531
- .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
+ }
2532
2719
  // background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
2533
2720
  // 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
2534
2721
  // 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
@@ -2536,72 +2723,250 @@ export class SubagentService {
2536
2723
  if (notifyGateAllowsDelivery(record.closedReason)) {
2537
2724
  this.collectCoordinator.route(record);
2538
2725
  }
2539
- })
2540
- .catch((err: unknown) => {
2541
- // detached 吞错:轮次执行内部已 finalize record(含 emitPendingUnregister),
2542
- // finalizeRecord manifest 写入已降级为 best-effort(失败仅 logger.error + appendEntry,
2543
- // 不外抛)。因此此处不应走到——但作为最后一道兼底,记录调试日志后吞下,不外抛。
2544
- // 完成通知由 finalizeRecord 内的 emitPendingUnregister 承担(pending-notifications 消费)。
2545
- // cancel 抢先时 status=cancelled,cancelBackground 自己 emit,此处无需重复。
2546
- // 交接包未被消费(engine.run 前置 throw 的极端形态)时的泄漏兜底。
2547
- this.chatRoundTickets.delete(record.id);
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);
2735
+ }
2548
2736
  if (err instanceof Error) {
2549
- logger.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
2737
+ logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
2550
2738
  }
2551
- });
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
+ })();
2749
+ }
2750
+
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
+ }
2552
2803
  }
2553
2804
 
2554
- /** PiEngine 的编排服务适配器(chat 绑定):闭包持有本实例的编排面——chat 轮次交接
2555
- * 由此成为 PiEngineService 的可选面(SAR 直绑 Service 的 workflow 实例不提供,
2556
- * Service 公共接口不为引擎内部交接扩面)。 */
2557
- private piEngineServiceAdapter(): PiEngineService {
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 {
2558
2953
  return {
2559
2954
  executeAndAwait: (opts, signal, onEvent, stream) => this.executeAndAwait(opts, signal, onEvent, stream),
2560
2955
  getRecordForAction: (id) => this.getRecordForAction(id),
2561
2956
  closeSubagent: (record, force) => this.closeSubagent(record, force),
2562
2957
  cancel: (id) => this.cancel(id),
2563
2958
  collectRecords: (limit, statusFilter) => this.collectRecords(limit, statusFilter),
2564
- takeChatRound: (taskId) => this.takeChatTicket(taskId),
2565
- runChatRound: (ticket) => this.runTicketRound(ticket),
2566
- resumeChatRound: (record, text) => this.resumeColdRound(record, text),
2567
2959
  reportRecordTransition: (record) => this.store.reportRecordTransition(record),
2568
2960
  };
2569
2961
  }
2570
2962
 
2571
- /** [D4 聚合连带] PiEngineService 的显式结构视图(registration / SAR 直绑消费)。
2572
- * 原两绑定点把 SubagentService 整体结构化兼容为 PiEngineService——依赖查询/交互
2573
- * 方法 public;聚合收窄后改经本视图显式适配(成员集合与原直绑等价,行为零差异)。
2574
- * getter 形态:face 视图(惰性构造的适配对象)而非动作方法。 */
2575
- get asEngineService(): PiEngineService {
2963
+ /** [D4 聚合连带] 引擎服务面的显式结构视图(SAR 构造 resolveHostPiEnginePort
2964
+ * 传入的 getService 兼容位消费——W3 后该面无引擎侧消费方,保留使 SAR 调用点
2965
+ * 零改动,W8 收口时随签名一并清理)。getter 形态:face 视图(惰性构造)。 */
2966
+ get asEngineService(): HostBridgeServiceFace {
2576
2967
  return this.piEngineServiceAdapter();
2577
2968
  }
2578
2969
 
2579
- /** chat 域轮次交接的消费侧(PiEngine.run 回调):一次性取走,防重复消费。私有名与
2580
- * PiEngineService 可选面成员不同名(历史约束:D4 聚合前的结构化直绑时代,私有同名
2581
- * 成员会阻断 SubagentService 直绑;聚合后经 asEngineService 显式视图,约束已消失,
2582
- * 不同名保留为与 adapter 显式映射一致的命名纪律)。 */
2583
- private takeChatTicket(taskId: string): ChatRoundTicket | undefined {
2584
- const ticket = this.chatRoundTickets.get(taskId);
2585
- if (ticket !== undefined) this.chatRoundTickets.delete(taskId);
2586
- return ticket;
2587
- }
2588
-
2589
- /** PiEngine chat 分支的执行回调:原 runAndFinalize 直调链的 EnginePort 化落点
2590
- * (编排归 Service——pool 槽 + runSpawn + 终态迁移,行为零变化)。 */
2591
- private async runTicketRound(ticket: ChatRoundTicket): Promise<AgentResult> {
2592
- return this.runAndFinalize(
2593
- ticket.record,
2594
- ticket.opts,
2595
- ticket.ctx,
2596
- ticket.identity,
2597
- ticket.signal,
2598
- ticket.priority,
2599
- undefined,
2600
- ticket.stream,
2601
- ticket.resume,
2602
- );
2603
- }
2604
-
2605
2970
  /**
2606
2971
  * 取消 background record。CAS 抢锁(tryTransition)——抢到则 notify + 写 tombstone;
2607
2972
  * 没抢到(detached 已 finalize,record 已终态)返回 false,不触碰任何收尾副作用。
@@ -2624,9 +2989,17 @@ export class SubagentService {
2624
2989
  // [T2④ / LC-2] 裸 SIGTERM 收敛到 killRecordChildWithEscalation:SIGTERM 被无视时
2625
2990
  // 30s 升级 SIGKILL——cancel 语义 = 进程必死,不能赌 SIGTERM 生效(record 已终态化,
2626
2991
  // 此后无其他回收通道)。settled watchdog 同步撤下(等待窗口随取消终结,幂等)。
2992
+ // [W3] chatMode 的实际终止经协议 interact cancel(引擎进程内 SIGTERM → settle
2993
+ // 等待 → 杀链升级;首轮在途 run 的 abort 帧由 controller.signal 经 RemoteEngine
2994
+ // cancel 分级另行承载,两路幂等)。
2627
2995
  killRecordChildWithEscalation(record.id, "cancelBackground");
2628
2996
  disarmIdleTimer(record.id);
2629
2997
  disarmSettledWatchdog(record.id);
2998
+ disarmRoundFromProtocol(record.id);
2999
+ this.unregisterChatRoundRoute(record.id);
3000
+ if (record.chatMode) {
3001
+ this.terminateChatSession(record, "cancel", "cancelBackground");
3002
+ }
2630
3003
  // [A2-1] CAS 抢锁防重复收尾:running 才放行。doFinalizeRecord Step 0 await 窗口内
2631
3004
  // record 可能已被终态化(closed)但尚未 archive——没抢到说明 detached 已 finalize,
2632
3005
  // cancel 来晚了,闭嘴返回 false(completeRecord 会覆写终态 + tombstone/finalized
@@ -2688,6 +3061,10 @@ export class SubagentService {
2688
3061
  modelService: this.modelService,
2689
3062
  pi: this.pi,
2690
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),
2691
3068
  sessionDir: this.sessionsDir,
2692
3069
  },
2693
3070
  record,
@@ -2721,7 +3098,13 @@ export class SubagentService {
2721
3098
 
2722
3099
  /** run() 创建期异常的收尾(H1 修复):createAndConfigureSession 失败会抛,本方法合成 failed
2723
3100
  * AgentResult → CAS 抢锁 → finalizeRecord(与正常路径同形)。返回合成 result 供 runAndFinalize
2724
- * 继续返回(不 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 是宿主数据不清理)。 */
2725
3108
  private async finalizeFailed(record: ExecutionRecord, err: unknown): Promise<AgentResult> {
2726
3109
  const errMsg = toErrorMessage(err);
2727
3110
  // durationMs 用真实耗时(startedAt → now),避免失败统计恒为 0 失真。
@@ -2796,43 +3179,6 @@ export class SubagentService {
2796
3179
  );
2797
3180
  }
2798
3181
  }
2799
-
2800
- /** 构造 SessionRunnerContext(spawn 模式:无需 SDK 实例)。 */
2801
- private buildSessionRunnerContext(overrideCwd?: string): SessionRunnerContext {
2802
- return {
2803
- cwd: overrideCwd ?? this.cwd,
2804
- agentDir: this.modelService.getAgentDir(),
2805
- // ADR-031 废弃 discovery.json 后,skillDirs 为空。子 session 的 --skill
2806
- // 由 agent({skill}) 调用方显式传入(resolveSkillPath → opts.skillPath)。
2807
- skillDirs: [],
2808
- mainCwd: this.cwd,
2809
- // mainSessionFile: fork source 解析用,从 session_start 缓存获取。
2810
- mainSessionFile: this.getMainSessionFile?.() ?? undefined,
2811
- // worktree pid 回调:session-runner first header 时补全注册表 pid。
2812
- onWorktreePid: (branch: string, pid: number, sessionFile?: string) => this.worktreeManager.registerPid(branch, pid, sessionFile),
2813
- uiRequestHandler: this.uiRequestHandler,
2814
- // SR-4:L2 dialog 队列透传——child close 时 session-runner 据此调 rejectChildDialogs
2815
- // 清理 L2 pending dialog,防全局死锁。undefined 时 session-runner 跳过 L2 清理。
2816
- dialogQueue: this.dialogQueue,
2817
- // 主进程运行模式:session-runner W4 守卫据此决定是否注入 ask_user RPC 提示词。
2818
- mode: this.uiObservability.getMode(),
2819
- // [递归可见性] 透传所属根 session(runSpawn 注入为子进程 env PI_SUBAGENT_ROOT_SESSION_ID)。
2820
- // sessionRootId 在 initSession 设定(根进程=sessionId,子进程=env 贯穿的真 ROOT)。
2821
- // execute/executeAndAwait 调本方法前必经 initSession,此时 sessionRootId 已非空;
2822
- // ?? 兑底防类型漂移(运行时不可达)。
2823
- sessionRootId: this.sessionRootId ?? this.sessionId ?? "",
2824
- // [MF-3] 透传 ROOT cwd(runSpawn 落盘目录编码键 + 注入子进程 env PI_SUBAGENT_ROOT_CWD)。
2825
- // worktree 模式下 mainCwd = 本进程 checkout 路径,rootCwd 才是真 ROOT——session 文件
2826
- // 落盘统一用 rootCwd 编码,ROOT 磁盘重建才扫得到深层 record(与 sessionRootId 同构)。
2827
- rootCwd: this.rootCwd,
2828
- // [V2 决策 2] chatMode 首轮闭环:agent_settled 时 session-runner 调本回调。
2829
- // [D4-②] 业务闭包已拆至 round-settlement.ts(this.settleRound 字段工厂构造,
2830
- // deps 回调注入 notifyComplete / reportRecordTransition / closeAfterRoundSettled,
2831
- // 行为逐字节等价)——轻量 idle 化语义(round+=1 + notify 主 agent,不调
2832
- // doFinalizeRoundToIdle)与增量派生/base 推进/哨兵的机制注释见该文件。
2833
- onRoundSettled: this.settleRound,
2834
- };
2835
- }
2836
3182
  }
2837
3183
 
2838
3184
  // ── 进程单例访问器 ────────────────────────────────────