@zhushanwen/subagent-core 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-Dv4QhSJ_.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +835 -668
  6. package/dist/{types-Dv4QhSJ_.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +835 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14123 -15467
  20. package/dist/index.d.cts +1179 -1540
  21. package/dist/index.d.ts +1179 -1540
  22. package/dist/index.js +7694 -10986
  23. package/dist.bundle/index.cjs +33485 -0
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/agent-registry.test.ts +1 -1
  30. package/src/execution/__tests__/alive-store.test.ts +1 -1
  31. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  32. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  33. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  34. package/src/execution/__tests__/collect-coordinator-service.test.ts +316 -0
  35. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  36. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +229 -0
  37. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  38. package/src/execution/__tests__/config.test.ts +1 -1
  39. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  40. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  41. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  42. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +21 -34
  43. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  44. package/src/execution/__tests__/gc-timer.test.ts +8 -23
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  46. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  47. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  48. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  49. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  50. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  52. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  53. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  54. package/src/execution/__tests__/notify-ledger.test.ts +27 -0
  55. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  56. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  57. package/src/execution/__tests__/record-store.test.ts +60 -6
  58. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  59. package/src/execution/__tests__/relay-env.test.ts +12 -1
  60. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  61. package/src/execution/__tests__/session-file-gc.test.ts +1 -1
  62. package/src/execution/__tests__/session-pending.test.ts +119 -17
  63. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  64. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  65. package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
  66. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +14 -11
  67. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  69. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  70. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  71. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  72. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  73. package/src/execution/__tests__/sync-collect-recovery.test.ts +1102 -0
  74. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  75. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  76. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  77. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  78. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  79. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  80. package/src/execution/agent-registry.ts +6 -2
  81. package/src/execution/collect-coordinator.ts +200 -0
  82. package/src/execution/config.ts +77 -5
  83. package/src/execution/dialog-queue.ts +6 -73
  84. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  85. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  86. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  87. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  88. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  89. package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
  90. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  91. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  92. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  93. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  94. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  95. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  96. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  97. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  98. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  99. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +82 -55
  100. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  101. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  102. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  103. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +8 -3
  104. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  105. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  106. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  107. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  108. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  109. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  110. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  111. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  112. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  113. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  114. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  115. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  116. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  117. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  118. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  119. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  120. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  121. package/src/execution/engine/client/client-options.ts +103 -0
  122. package/src/execution/engine/client/engine-client.ts +687 -0
  123. package/src/execution/engine/client/mirror.ts +167 -0
  124. package/src/execution/engine/client/pid-file.ts +392 -0
  125. package/src/execution/engine/client/reaper.ts +95 -0
  126. package/src/execution/engine/client/remote-engine.ts +441 -0
  127. package/src/execution/engine/client/reverse-router.ts +255 -0
  128. package/src/execution/engine/common/capability-gate.ts +109 -22
  129. package/src/execution/engine/common/errors.ts +9 -25
  130. package/src/execution/engine/common/event-journal.ts +4 -50
  131. package/src/execution/engine/common/journal-replay.ts +3 -1
  132. package/src/execution/engine/common/kill-chain.ts +18 -251
  133. package/src/execution/engine/common/nesting-guard.ts +18 -114
  134. package/src/execution/engine/common/pool-manager.ts +4 -0
  135. package/src/execution/engine/common/session-view-service.ts +56 -84
  136. package/src/execution/engine/config.ts +151 -0
  137. package/src/execution/engine/d8-compat.ts +346 -0
  138. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  139. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  140. package/src/execution/engine/engine-discovery.ts +30 -7
  141. package/src/execution/engine/engine-inspect-package.ts +282 -0
  142. package/src/execution/engine/engine-manifest.ts +244 -0
  143. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  144. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  145. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  146. package/src/execution/engine/host/host-bridge.ts +150 -0
  147. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  148. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  149. package/src/execution/engine/host/spawned-children.ts +139 -0
  150. package/src/execution/engine/host-task-spec.ts +29 -2
  151. package/src/execution/engine/model-validation.ts +55 -2
  152. package/src/execution/engine/paths.ts +16 -41
  153. package/src/execution/engine/port.ts +48 -2
  154. package/src/execution/engine/registry.ts +153 -34
  155. package/src/execution/engine/routing.ts +225 -52
  156. package/src/execution/engine/types.ts +25 -200
  157. package/src/execution/execution-record.ts +7 -0
  158. package/src/execution/finalize-record.ts +40 -11
  159. package/src/execution/idle-gc.ts +89 -14
  160. package/src/execution/lifecycle-manager.ts +10 -7
  161. package/src/execution/lifecycle-predicates.ts +10 -8
  162. package/src/execution/notifier.ts +234 -25
  163. package/src/execution/notify-host.ts +21 -1
  164. package/src/execution/record-entry.ts +16 -0
  165. package/src/execution/record-store.ts +189 -31
  166. package/src/execution/relay-env.ts +19 -36
  167. package/src/execution/round-supervisor/domain.ts +89 -0
  168. package/src/execution/round-supervisor/index.ts +33 -0
  169. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  170. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  171. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  172. package/src/execution/round-supervisor/service-binding.ts +273 -0
  173. package/src/execution/round-supervisor/supervisor.ts +426 -0
  174. package/src/execution/session-pending.ts +68 -74
  175. package/src/execution/settled-watchdog.ts +39 -0
  176. package/src/execution/subagent-actions-core.ts +62 -6
  177. package/src/execution/subagent-service.ts +1161 -406
  178. package/src/execution/subprocess-agent-runner.ts +16 -8
  179. package/src/execution/sync-rebuild.ts +87 -0
  180. package/src/execution/types.ts +104 -117
  181. package/src/execution/ui-channels.ts +13 -214
  182. package/src/execution/ui-request-handler-factory.ts +7 -4
  183. package/src/execution/ui-request-observability.ts +2 -2
  184. package/src/execution/workflow-state-root.ts +67 -0
  185. package/src/execution/worktree-git-ops.ts +5 -0
  186. package/src/execution/worktree-manager.ts +11 -1
  187. package/src/index.ts +55 -26
  188. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  189. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  190. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  191. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  192. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  193. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  194. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  195. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  196. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  197. package/src/orchestration/execute-agent-call.ts +1 -1
  198. package/src/orchestration/file-run-store.ts +144 -59
  199. package/src/orchestration/models/types.ts +43 -42
  200. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  201. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
  202. package/src/shared/zcode-model-ref.ts +31 -0
  203. package/dist/chunk-43ONBFZX.js +0 -240
  204. package/dist/chunk-A4IVZWVX.js +0 -31
  205. package/dist/chunk-APZY4IME.js +0 -27
  206. package/dist/chunk-V3VHZ2VX.js +0 -59
  207. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  208. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  209. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  210. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  211. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  212. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  213. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  214. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  215. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  216. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  217. package/src/__tests__/session-runner.test.ts +0 -69
  218. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  219. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  220. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  221. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  222. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  223. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  224. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  225. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  226. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  227. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  228. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  229. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  230. package/src/execution/__tests__/output-collector.test.ts +0 -469
  231. package/src/execution/__tests__/pi-invocation.test.ts +0 -134
  232. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  233. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  234. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  235. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  236. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  237. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  238. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  239. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  240. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  241. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  242. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  243. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  244. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  245. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  246. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  247. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  248. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  249. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  250. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  251. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  252. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  253. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  254. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  255. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  256. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  257. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  258. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  259. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  260. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  261. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  262. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  263. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  264. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  265. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  266. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  267. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  268. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  269. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  270. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  271. package/src/execution/engine/common/schema-emulation.ts +0 -189
  272. package/src/execution/engine/common/session-view-projection.ts +0 -51
  273. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  274. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  275. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  276. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  277. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  278. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  279. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  280. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  281. package/src/execution/engine/engines/pi/reader.ts +0 -59
  282. package/src/execution/engine/engines/pi/registration.ts +0 -37
  283. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  284. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  285. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  286. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  287. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  288. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  289. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  290. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  291. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  292. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  293. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  294. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  295. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  296. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  297. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  298. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  299. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  300. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  301. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  302. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  303. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  304. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  305. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  306. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  307. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  308. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  309. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  310. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  311. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  312. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  313. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  314. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  315. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  316. package/src/execution/round-settlement.ts +0 -93
  317. package/src/execution/ui-request-queue.ts +0 -215
  318. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,687 @@
1
+ // src/execution/engine/client/engine-client.ts
2
+ //
3
+ // EngineClient:协议客户端(W2,impl-plan §2.2;与 zcode AppServerConnection 同型
4
+ // 但引擎无关)。职责 = spawn 引擎 CLI / NDJSON 帧编解码 / 请求-应答 id 关联 /
5
+ // 反向请求路由(见 reverse-router.ts)/ 崩溃重建(≤3 次指数退避 1s/2s/4s)/
6
+ // dispose / killAll / stdout 行解析 + stderr 常驻排空(仅内存环缓冲尾 400 字符)。
7
+ //
8
+ // spawn 平台参数(impl-plan §2.2 必写死):POSIX 引擎 CLI 以独立进程组 spawn
9
+ // (detached:true,进程组收割前提);Windows detached:false + windowsHide:true,
10
+ // 收割走 taskkill /PID <pid> /T /F。spawn 点区分:SDK spawnEngineChild 是引擎包内
11
+ // 「任务子进程」原语(硬编码 detached:false 且禁 detached 键,impl-plan §2.12);
12
+ // 本类 spawn 的是引擎 CLI 进程本身(impl-plan §2.2 detached:true),二者是协议两侧
13
+ // 的两个 spawn 点,故此处直接用 node:child_process。env 仍必经 SDK
14
+ // buildEngineChildEnv(三层契约,DAG 边 W12→W2)。
15
+ //
16
+ // 引擎自灭 core 侧配套:spawn 不覆写引擎 stdin(stdio[0] = 'pipe' 独占管道,core
17
+ // 是唯一写端)——引擎侧 stdio EOF 主判据(SDK armEngineSelfDestruct)的前提。
18
+ // 超时域划分(R9-2 / R9-2b)的实现与注释在 reverse-router.ts(反向面)与本文件
19
+ // 正向请求段;run 不设墙钟(任务级默认无超时,AGENTS.md 规则 19 / ADR-0047)。
20
+
21
+ import { spawn } from "node:child_process";
22
+ import type { ChildProcess } from "node:child_process";
23
+
24
+ import {
25
+ buildEngineChildEnv,
26
+ CANCEL_SETTLE_GRACE_MS,
27
+ CRASH_REBUILD_BACKOFF_MS,
28
+ CRASH_REBUILD_MAX_ATTEMPTS,
29
+ ENGINE_PROTOCOL_VERSION,
30
+ EngineSdkError,
31
+ HANDSHAKE_TIMEOUT_MS,
32
+ getLogger,
33
+ isProtocolVersionCompatible,
34
+ isResponseFrame,
35
+ isNotificationFrame,
36
+ isReverseRequestFrame,
37
+ STDERR_TAIL_CHARS,
38
+ engineProtocolMismatchError,
39
+ hostKindOf,
40
+ resolveEngineNodeLaunch,
41
+ type EngineHandleData,
42
+ type InitializeResult,
43
+ type RequestFrame,
44
+ } from "@zhushanwen/subagent-engine-sdk";
45
+
46
+ import { SpawnedChildrenMirror } from "./mirror.ts";
47
+ import {
48
+ removePidfile,
49
+ registerEnginePidfile,
50
+ sweepEnginePidfiles,
51
+ } from "./pid-file.ts";
52
+ import { routeReverseRequest, type ReverseRouterDeps } from "./reverse-router.ts";
53
+ import {
54
+ defaultEngineCmdlineMatcher,
55
+ warnOnManifestDiagnostics,
56
+ type EngineClientOptions,
57
+ } from "./client-options.ts";
58
+ import { killProcessTree, waitForChildExit } from "./reaper.ts";
59
+
60
+ const logger = getLogger("subagents");
61
+
62
+ /** dispose 帧等待上界(设计 §3.6:dispose 上界 3s,超时即杀)。 */
63
+ const DISPOSE_GRACE_MS = 3_000;
64
+ /** SIGKILL 后收尸等待上限(kill-chain 同型有界兜底,reaper.waitForChildExit 消费)。 */
65
+ const SIGKILL_REAP_TIMEOUT_MS = 10_000;
66
+ /** 协议帧违规日志的回显截断长度(非 NDJSON 行 / 未知帧的诊断留痕)。 */
67
+ const FRAME_ECHO_MAX_CHARS = 200;
68
+
69
+ /** 客户端连接状态。 */
70
+ export type EngineClientState =
71
+ | "idle"
72
+ | "connecting"
73
+ | "ready"
74
+ | "exited"
75
+ | "unavailable"
76
+ | "disposed";
77
+
78
+ /**
79
+ * run 作用域反向通知路由(RemoteEngine.run 注册,终态后注销)。
80
+ * 回调允许返回 Promise(数据面 10s 应答守卫的计时对象;同步回调不受守卫影响)。
81
+ * [W3 v1.x] onRoundLifecycle:host/roundLifecycle 消费口(首轮 runId 键;续聊轮
82
+ * recordId 键走 registerRecordRoute,D1-A 关联键分路)。
83
+ */
84
+ export interface RunRoute {
85
+ onEvent?: (event: unknown) => void | Promise<void>;
86
+ onStreamDelta?: (delta: string) => void | Promise<void>;
87
+ onPoolResolved?: (poolKey: string) => void | Promise<void>;
88
+ onHandleReady?: (
89
+ partial: Pick<EngineHandleData, "sessionRef" | "poolKey">,
90
+ ) => void | Promise<void>;
91
+ onRoundLifecycle?: (phase: unknown) => void | Promise<void>;
92
+ }
93
+
94
+ /** spawn 引擎 CLI 平台形态(impl-plan §2.2 必写死的两行)。 */
95
+ interface EngineSpawnPlatformOptions {
96
+ detached: boolean;
97
+ windowsHide: boolean;
98
+ }
99
+
100
+ function engineSpawnPlatformOptions(): EngineSpawnPlatformOptions {
101
+ if (process.platform === "win32") {
102
+ return { detached: false, windowsHide: true };
103
+ }
104
+ return { detached: true, windowsHide: false };
105
+ }
106
+
107
+ interface PendingRequest {
108
+ resolve: (result: unknown) => void;
109
+ reject: (err: Error) => void;
110
+ timer: NodeJS.Timeout | undefined;
111
+ }
112
+
113
+ /** 引擎进程意外退出 / 被杀时在途请求的统一错误(附 stderr 尾)。 */
114
+ function engineCrashedError(detail: string, stderrTail: string): EngineSdkError {
115
+ const tail = stderrTail.length > 0 ? ` stderr tail: ${stderrTail}` : "";
116
+ return new EngineSdkError(
117
+ "engine_crashed",
118
+ `engine process exited unexpectedly: ${detail}.${tail}`,
119
+ "The engine will be rebuilt (up to 3 attempts with exponential backoff) on the next run. "
120
+ + "If it keeps crashing, inspect the engine package installation and its stderr diagnostics.",
121
+ { stderrTail },
122
+ );
123
+ }
124
+
125
+ /**
126
+ * 引擎协议客户端(per engine id 一份;同宿主进程内不共享——两宿主各自 spawn)。
127
+ */
128
+ export class EngineClient {
129
+ readonly engineId: string;
130
+ /** spawnedChildren 状态镜像(host/childSpawned / childStateChanged 的落地面)。 */
131
+ readonly mirror = new SpawnedChildrenMirror();
132
+
133
+ private readonly opts: EngineClientOptions;
134
+ private readonly reverseRouterDeps: ReverseRouterDeps;
135
+ private state: EngineClientState = "idle";
136
+ private child: ChildProcess | undefined;
137
+ private nextRequestId = 1;
138
+ private readonly pending = new Map<number, PendingRequest>();
139
+ private readonly runRoutes = new Map<string, RunRoute>();
140
+ /** [W3 v1.x] chat 轮次路由(recordId 键,D1-A——续聊轮无 runId);注册经
141
+ * RemoteEngine.registerChatRoundRoute,record 终态注销。 */
142
+ private readonly recordRoutes = new Map<string, RunRoute>();
143
+ private stderrTail = "";
144
+ private unavailableReason: EngineSdkError | undefined;
145
+ private connectInFlight: Promise<void> | undefined;
146
+ private pidfileWritten: string | undefined;
147
+ private pidfileSwept = false;
148
+ private intentionalKill = false;
149
+ /** run 期 host/handleReady 的最近回填(崩溃时在途 run 的合成 handle 数据源)。 */
150
+ private lastPartialHandle: { sessionRef: Record<string, string>; poolKey: string } | undefined;
151
+ private initializeDiagnostics: InitializeResult | undefined;
152
+ private stdoutBuffer = "";
153
+
154
+ constructor(opts: EngineClientOptions) {
155
+ this.opts = opts;
156
+ this.engineId = opts.engineId;
157
+ this.reverseRouterDeps = {
158
+ engineId: opts.engineId,
159
+ uiRequestHandler: opts.uiRequestHandler,
160
+ permissionHandler: opts.permissionHandler,
161
+ log: opts.log,
162
+ runRoutes: this.runRoutes,
163
+ recordRoutes: this.recordRoutes,
164
+ mirror: this.mirror,
165
+ setPartialHandle: (partial) => {
166
+ this.lastPartialHandle = partial;
167
+ },
168
+ sendResponse: (id, result) => this.sendResponse(id, result),
169
+ failEngine: (reason) => this.killAll(reason),
170
+ isDisposed: () => this.disposed,
171
+ };
172
+ this.mirror.onChange((event) => {
173
+ opts.onMirrorChanged?.(event);
174
+ });
175
+ }
176
+
177
+ get currentState(): EngineClientState {
178
+ return this.state;
179
+ }
180
+
181
+ /** disposed 判定(getter 形态——TS 不对跨 await 的字段窄化保持,字段直比会在长方法内误窄化)。 */
182
+ private get disposed(): boolean {
183
+ return this.state === "disposed";
184
+ }
185
+
186
+ /** 引擎 CLI 进程 pid(未连接 = undefined)。 */
187
+ get enginePid(): number | undefined {
188
+ return this.child?.pid;
189
+ }
190
+
191
+ /** stderr 环形缓冲尾(engine_crashed 现场 / 诊断)。 */
192
+ get stderrTailText(): string {
193
+ return this.stderrTail;
194
+ }
195
+
196
+ /** 最近一次 host/handleReady 回填(RemoteEngine 崩溃合成 handle 用)。 */
197
+ getPartialHandle(): { sessionRef: Record<string, string>; poolKey: string } | undefined {
198
+ return this.lastPartialHandle;
199
+ }
200
+
201
+ /** initialize 应答(诊断面;RemoteEngine 诊断留痕用)。 */
202
+ getInitializeDiagnostics(): InitializeResult | undefined {
203
+ return this.initializeDiagnostics;
204
+ }
205
+
206
+ /**
207
+ * 确保引擎 CLI 已连接(spawn + initialize 握手)。幂等;崩溃后按重建状态机重试:
208
+ * 初建失败 → 退避 1s/2s/4s 各重建一次(共 1 + CRASH_REBUILD_MAX_ATTEMPTS 次
209
+ * spawn 尝试),全败标记不可用直到宿主重启(unavailable 恒 throw)。
210
+ * 版本协商越界 → engine_protocol_mismatch + 直接标记不可用(不重试)。
211
+ */
212
+ async ensureConnected(): Promise<void> {
213
+ if (this.disposed) {
214
+ throw new EngineSdkError(
215
+ "engine_crashed",
216
+ `engine client for "${this.engineId}" is disposed`,
217
+ "Create a new engine client / RemoteEngine instance.",
218
+ );
219
+ }
220
+ if (this.state === "unavailable") {
221
+ throw this.unavailableReason ?? new Error(`engine "${this.engineId}" is unavailable`);
222
+ }
223
+ if (this.state === "ready") return;
224
+ if (this.connectInFlight) return this.connectInFlight;
225
+
226
+ this.connectInFlight = this.connectWithRebuild();
227
+ try {
228
+ await this.connectInFlight;
229
+ } finally {
230
+ this.connectInFlight = undefined;
231
+ }
232
+ }
233
+
234
+ private async connectWithRebuild(): Promise<void> {
235
+ this.state = "connecting";
236
+ // 启动期清扫(每实例一次):宿主崩溃残留的同 id 引擎孤儿 + 陈旧 pidfile。
237
+ if (!this.pidfileSwept) {
238
+ this.pidfileSwept = true;
239
+ const sweep = sweepEnginePidfiles({
240
+ engineDataDir: this.opts.dataDir,
241
+ engineId: this.engineId,
242
+ currentHostPid: process.pid,
243
+ matchesEngineCmdline: this.opts.engineCmdlineMatcher
244
+ ?? defaultEngineCmdlineMatcher(this.opts.command),
245
+ });
246
+ if (sweep.killed.length > 0) {
247
+ logger.warn(
248
+ `[engine-client:${this.engineId}] startup sweep killed stale orphan engine pids: ${sweep.killed.join(",")}`,
249
+ );
250
+ }
251
+ for (const removed of sweep.removed) {
252
+ logger.debug(`[engine-client:${this.engineId}] swept stale pidfile ${removed.file}: ${removed.reason}`);
253
+ }
254
+ }
255
+
256
+ let attempt = 0;
257
+ // 序列:attempt 0 = 初建;1..CRASH_REBUILD_MAX_ATTEMPTS = 重建(退避 1s/2s/4s)。
258
+ while (attempt <= CRASH_REBUILD_MAX_ATTEMPTS) {
259
+ if (attempt > 0) {
260
+ const backoff = CRASH_REBUILD_BACKOFF_MS[attempt - 1];
261
+ logger.warn(
262
+ `[engine-client:${this.engineId}] rebuild attempt ${attempt}/${CRASH_REBUILD_MAX_ATTEMPTS} after ${backoff}ms backoff`,
263
+ );
264
+ await delay(backoff);
265
+ if (this.disposed) return;
266
+ }
267
+ try {
268
+ await this.spawnAndInitialize();
269
+ this.state = "ready";
270
+ return;
271
+ } catch (err) {
272
+ this.teardownProcess("handshake failed");
273
+ if (err instanceof EngineSdkError && err.code === "engine_protocol_mismatch") {
274
+ // 版本越界:重建无意义(每次都会越界)→ 直接标记不可用。
275
+ this.markUnavailable(err);
276
+ throw err;
277
+ }
278
+ attempt += 1;
279
+ if (attempt > CRASH_REBUILD_MAX_ATTEMPTS) {
280
+ const failure =
281
+ err instanceof Error ? err : new Error(String(err));
282
+ const unavailable = new EngineSdkError(
283
+ "engine_crashed",
284
+ `engine "${this.engineId}" failed to start after `
285
+ + `${1 + CRASH_REBUILD_MAX_ATTEMPTS} attempts (initial + ${CRASH_REBUILD_MAX_ATTEMPTS} rebuilds): ${failure.message}`,
286
+ "Fix or reinstall the engine package, then restart the host. The engine stays "
287
+ + "unavailable until the next host start.",
288
+ );
289
+ this.markUnavailable(unavailable);
290
+ throw unavailable;
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ private markUnavailable(reason: EngineSdkError): void {
297
+ this.state = "unavailable";
298
+ this.unavailableReason = reason;
299
+ logger.error(`[engine-client:${this.engineId}] marked unavailable: ${reason.message}`);
300
+ }
301
+
302
+ /** spawn 引擎 CLI + initialize 握手 + 版本协商 + 诊断留痕。 */
303
+ private async spawnAndInitialize(): Promise<void> {
304
+ this.intentionalKill = false;
305
+ const platformOpts = engineSpawnPlatformOptions();
306
+ // [W9] 启动解析(宿主 × 平台二维矩阵,impl-plan §2.9):spawn argv 先经 SDK
307
+ // resolveEngineNodeLaunch 解析——打包态 pi 宿主必须用注入执行器
308
+ // XYZ_AGENT_ENGINE_NODE(pi binary 不是 node 执行器)、runtime sidecar 用
309
+ // process.execPath、standalone 用 PATH node(缺 node / 探针失败 → engine_not_found)。
310
+ // Windows .cmd 入口在此改写为显式 cmd.exe /c + 参数数组(禁 shell:true)。
311
+ const baseEnv = this.opts.baseEnv ?? { ...process.env };
312
+ const launch = await resolveEngineNodeLaunch({
313
+ entryPath: this.opts.command,
314
+ args: this.opts.args,
315
+ hostKind: hostKindOf(this.opts.hostKind, baseEnv),
316
+ env: baseEnv,
317
+ });
318
+ const env = buildEngineChildEnv(baseEnv, {
319
+ dataDir: this.opts.dataDir,
320
+ engineNode: this.opts.engineNode ?? launch.command,
321
+ electronRunAsNode: this.opts.electronRunAsNode ?? launch.electronRunAsNode,
322
+ relay: this.opts.relay,
323
+ identityEnv: this.opts.identityEnv,
324
+ envPrefixes: this.opts.envPrefixes,
325
+ processEnv: this.opts.processEnv,
326
+ });
327
+
328
+ // stdin 恒 'pipe' 且独占(引擎自灭主判据 stdio EOF 的前提;不覆写、不继承)。
329
+ this.child = spawn(launch.command, launch.args, {
330
+ cwd: this.opts.cwd,
331
+ env,
332
+ ...platformOpts,
333
+ stdio: ["pipe", "pipe", "pipe"],
334
+ });
335
+ const child = this.child;
336
+ child.on("error", (err) => {
337
+ // spawn 本身失败(ENOENT 等):让 initialize 超时/写失败路径统一收口。
338
+ logger.warn(`[engine-client:${this.engineId}] spawn error: ${err.message}`);
339
+ this.appendStderrTail(`spawn error: ${err.message}\n`);
340
+ });
341
+ // 管道死亡竞态(coverage-gate 满载 flaky 根因,2026-09):引擎死后在途帧写入的
342
+ // EPIPE 以「异步 error 事件」投递——writeFrame 的同步 try/catch 接不到;stdin 无
343
+ // error 监听时 Node 对无监听 error 事件直接抛 uncaughtException(测试进程整轮
344
+ // 崩红、用例全绿)。挂监听吃掉预期内的管道死亡留 debug 痕迹;在途请求失败的
345
+ // 语义由 exit 事件 → teardownProcess 统一承担,不经管道错误路径。
346
+ for (const pipe of ["stdin", "stdout", "stderr"] as const) {
347
+ child[pipe]?.on("error", (err: Error) => {
348
+ logger.debug(`[engine-client:${this.engineId}] ${pipe} pipe error (engine died concurrently = expected): ${err.message}`);
349
+ });
350
+ }
351
+ child.stdout?.setEncoding("utf-8");
352
+ child.stdout?.on("data", (chunk: string) => this.onStdoutData(chunk));
353
+ child.stderr?.setEncoding("utf-8");
354
+ child.stderr?.on("data", (chunk: string) => this.appendStderrTail(chunk));
355
+ child.on("exit", (code, signal) => this.onEngineExit(code, signal));
356
+
357
+ if (child.pid !== undefined) {
358
+ this.pidfileWritten = registerEnginePidfile({
359
+ engineDataDir: this.opts.dataDir,
360
+ engineId: this.engineId,
361
+ hostKind: this.opts.hostKind,
362
+ hostPid: process.pid,
363
+ enginePid: child.pid,
364
+ });
365
+ }
366
+ this.attachFrameReader();
367
+
368
+ // initialize 握手(HANDSHAKE_TIMEOUT_MS;版本越界 → mismatch + 不可用)。
369
+ const initParams = {
370
+ protocolVersion: ENGINE_PROTOCOL_VERSION,
371
+ hostInfo: {
372
+ name: this.opts.hostKind,
373
+ version: this.opts.hostVersion ?? process.version,
374
+ dataRoot: this.opts.dataDir,
375
+ },
376
+ engineConfig: this.opts.engineConfig ?? {},
377
+ };
378
+ let result: InitializeResult;
379
+ try {
380
+ result = (await this.request("initialize", initParams, {
381
+ timeoutMs: HANDSHAKE_TIMEOUT_MS,
382
+ })) as InitializeResult;
383
+ } catch (err) {
384
+ if (err instanceof EngineSdkError && err.code === "engine_handshake_timeout") {
385
+ throw new EngineSdkError(
386
+ "engine_handshake_timeout",
387
+ `engine "${this.engineId}" did not answer initialize within ${HANDSHAKE_TIMEOUT_MS}ms`,
388
+ "Check that the engine package is executable and speaks engine-protocol v1. "
389
+ + "The engine is marked unavailable until it answers a handshake.",
390
+ );
391
+ }
392
+ throw err;
393
+ }
394
+
395
+ if (!isProtocolVersionCompatible(result.protocolVersion)) {
396
+ throw engineProtocolMismatchError(result.protocolVersion);
397
+ }
398
+
399
+ this.initializeDiagnostics = result;
400
+ warnOnManifestDiagnostics(this.engineId, this.opts.manifestDiagnostics, result);
401
+ }
402
+
403
+ // ─────────────────────────────────────────────────────────────────────────
404
+ // stdout 行解析与帧路由
405
+ // ─────────────────────────────────────────────────────────────────────────
406
+
407
+ private attachFrameReader(): void {
408
+ this.stdoutBuffer = "";
409
+ }
410
+
411
+ private onStdoutData(chunk: string): void {
412
+ this.stdoutBuffer += chunk;
413
+ let newlineIndex = this.stdoutBuffer.indexOf("\n");
414
+ while (newlineIndex >= 0) {
415
+ const line = this.stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
416
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
417
+ if (line.trim().length > 0) this.handleLine(line);
418
+ newlineIndex = this.stdoutBuffer.indexOf("\n");
419
+ }
420
+ // 帧间残片留 buffer(OS 管道背压语义:不做无界缓存——残片即半行,有界)。
421
+ }
422
+
423
+ private handleLine(line: string): void {
424
+ let frame: unknown;
425
+ try {
426
+ frame = JSON.parse(line);
427
+ } catch {
428
+ logger.warn(
429
+ `[engine-client:${this.engineId}] dropped non-NDJSON stdout line (protocol contract: stdout is NDJSON-only): ${truncate(line, FRAME_ECHO_MAX_CHARS)}`,
430
+ );
431
+ return;
432
+ }
433
+ if (isResponseFrame(frame)) {
434
+ this.onResponseFrame(frame);
435
+ return;
436
+ }
437
+ if (isNotificationFrame(frame)) {
438
+ this.onEventNotification(frame.params.runId, frame.params.event);
439
+ return;
440
+ }
441
+ if (isReverseRequestFrame(frame)) {
442
+ this.onReverseRequest(frame);
443
+ return;
444
+ }
445
+ logger.warn(
446
+ `[engine-client:${this.engineId}] dropped unrecognized frame: ${truncate(line, FRAME_ECHO_MAX_CHARS)}`,
447
+ );
448
+ }
449
+
450
+ private onResponseFrame(frame: { id: number | string; result?: unknown; error?: unknown }): void {
451
+ if (typeof frame.id !== "number") return; // 反向请求的应答由 core 发出,不走本表
452
+ const pending = this.pending.get(frame.id);
453
+ if (!pending) return; // 迟到应答(已超时被拒)——丢弃
454
+ this.pending.delete(frame.id);
455
+ if (pending.timer !== undefined) clearTimeout(pending.timer);
456
+ const error = frame.error as { code?: string; message?: string; recovery?: string } | undefined;
457
+ if (error !== undefined && typeof error === "object") {
458
+ pending.reject(
459
+ new EngineSdkError(
460
+ typeof error.code === "string" ? error.code : "engine_unknown",
461
+ typeof error.message === "string" ? error.message : JSON.stringify(error),
462
+ typeof error.recovery === "string" ? error.recovery : "Inspect the engine error and retry.",
463
+ ),
464
+ );
465
+ return;
466
+ }
467
+ pending.resolve(frame.result);
468
+ }
469
+
470
+ private onEventNotification(runId: string, event: unknown): void {
471
+ this.runRoutes.get(runId)?.onEvent?.(event);
472
+ }
473
+
474
+ /** 帧④入口(路由实现见 reverse-router.ts——超时域二分的 core 侧语义所在)。 */
475
+ private onReverseRequest(frame: { id: string; method: string; params: unknown }): void {
476
+ routeReverseRequest(this.reverseRouterDeps, frame);
477
+ }
478
+
479
+ // ── 正向请求 ────────────────────────────────────────────────────────────
480
+
481
+ /**
482
+ * 发帧①并等帧②。opts.timeoutMs = 数据面超时域(initialize 等);不传 = 任务级
483
+ * 默认无超时(run,AGENTS.md 规则 19 / ADR-0047)。
484
+ */
485
+ request(method: string, params: unknown, opts?: { timeoutMs?: number }): Promise<unknown> {
486
+ if (this.state === "disposed" || this.state === "unavailable") {
487
+ return Promise.reject(
488
+ this.unavailableReason
489
+ ?? new EngineSdkError(
490
+ "engine_crashed",
491
+ `engine client for "${this.engineId}" is ${this.state}`,
492
+ "Rebuild the connection via ensureConnected or recreate the client.",
493
+ ),
494
+ );
495
+ }
496
+ const id = this.nextRequestId++;
497
+ const frame: RequestFrame = { id, method, params };
498
+ return new Promise<unknown>((resolve, reject) => {
499
+ const pending: PendingRequest = { resolve, reject, timer: undefined };
500
+ if (opts?.timeoutMs !== undefined) {
501
+ pending.timer = setTimeout(() => {
502
+ this.pending.delete(id);
503
+ reject(
504
+ new EngineSdkError(
505
+ method === "initialize" ? "engine_handshake_timeout" : "engine_request_timeout",
506
+ `engine "${this.engineId}" did not answer ${method} (id=${id}) within ${opts.timeoutMs}ms`,
507
+ "Retry the operation; if it persists the engine process is faulted and will be rebuilt.",
508
+ ),
509
+ );
510
+ }, opts.timeoutMs);
511
+ }
512
+ this.pending.set(id, pending);
513
+ if (!this.writeFrame(frame)) {
514
+ this.pending.delete(id);
515
+ if (pending.timer !== undefined) clearTimeout(pending.timer);
516
+ reject(
517
+ engineCrashedError(
518
+ `stdin write failed while sending ${method}`,
519
+ this.stderrTail,
520
+ ),
521
+ );
522
+ }
523
+ });
524
+ }
525
+
526
+ /** 注册 run 作用域反向通知路由;返回注销函数。 */
527
+ registerRunRoute(runId: string, route: RunRoute): () => void {
528
+ this.runRoutes.set(runId, route);
529
+ return () => {
530
+ if (this.runRoutes.get(runId) === route) this.runRoutes.delete(runId);
531
+ };
532
+ }
533
+
534
+ /** [W3 v1.x] 注册 chat 轮次路由(recordId 键);分发见 reverse-router.ts
535
+ * (与 runRoutes 同构,键空间互斥:runId=run 帧分配 / recordId=宿主铸造)。 */
536
+ registerRecordRoute(recordId: string, route: RunRoute): () => void {
537
+ this.recordRoutes.set(recordId, route);
538
+ return () => {
539
+ if (this.recordRoutes.get(recordId) === route) this.recordRoutes.delete(recordId);
540
+ };
541
+ }
542
+
543
+ /** 健康检查(ADR-0047:静默 ≠ 卡死,不据此杀任务)。 */
544
+ async ping(): Promise<void> {
545
+ await this.ensureConnected();
546
+ await this.request("ping", {});
547
+ }
548
+
549
+ /** cancel 受理窗口 = CANCEL_SETTLE_GRACE_MS;终态收敛由调用方(RemoteEngine)等 run 应答。 */
550
+ async cancelRun(runId: string, reason: string): Promise<void> {
551
+ await this.request("cancel", { runId, reason }, { timeoutMs: CANCEL_SETTLE_GRACE_MS });
552
+ }
553
+
554
+ // ── 收割 / 停机 ──────────────────────────────────────────────────────────
555
+
556
+ /**
557
+ * 宿主收割入口(D8 killAllSpawnedChildren 落点):组杀引擎 CLI(POSIX 负 pid 组杀 /
558
+ * Windows taskkill /T /F)+ 镜像整体置死 + pidfile 清理。在途请求以 engine_crashed
559
+ * 失败。「零残留」断言范围 = 一代子进程 + 组内后代(引擎自身 detached 后代不覆盖,
560
+ * impl-plan §7.2 R9-1 / 设计 §3.9 已接受代价)。
561
+ */
562
+ async killAll(reason: string): Promise<void> {
563
+ this.intentionalKill = true;
564
+ const child = this.child;
565
+ if (child !== undefined && child.pid !== undefined && child.exitCode === null) {
566
+ killProcessTree(child.pid, reason, { engineId: this.engineId });
567
+ await waitForChildExit(child, SIGKILL_REAP_TIMEOUT_MS);
568
+ }
569
+ this.teardownProcess(reason);
570
+ }
571
+
572
+ /**
573
+ * 协议 dispose:dispose 帧(3s 上界)→ 组杀兜底 → 清理。幂等。
574
+ * dispose 后首个 run 的重建由新的连接状态机承担(EnginePort.dispose 契约)。
575
+ */
576
+ async dispose(): Promise<void> {
577
+ if (this.state === "disposed") return;
578
+ if (this.state === "ready" && this.child !== undefined && this.child.exitCode === null) {
579
+ try {
580
+ await this.request("dispose", {}, { timeoutMs: DISPOSE_GRACE_MS });
581
+ } catch (err) {
582
+ // dispose 帧失败(超时/进程已死)→ 直接走杀链兜底,debug 留痕。
583
+ logger.debug(
584
+ `[engine-client:${this.engineId}] dispose frame failed, falling back to kill chain: ${
585
+ err instanceof Error ? err.message : String(err)
586
+ }`,
587
+ );
588
+ }
589
+ }
590
+ await this.killAll("dispose");
591
+ this.state = "disposed";
592
+ }
593
+
594
+ /** 引擎进程 exit:镜像整体置死 + pidfile 清理 + 在途请求失败 + 状态回落。 */
595
+ private onEngineExit(code: number | null, signal: NodeJS.Signals | null): void {
596
+ const detail = signal !== null ? `signal ${signal}` : `exit code ${code}`;
597
+ // intentionalKill 消费点:主动杀(killAll/dispose)后的 exit 是预期收敛(debug);
598
+ // 非主动杀 = 意外崩溃(warn 出声,供排障区分「引擎自己挂了」与「宿主收割」)。
599
+ if (this.intentionalKill) {
600
+ logger.debug(`[engine-client:${this.engineId}] engine exited after intentional kill (${detail}) — expected`);
601
+ } else {
602
+ logger.warn(`[engine-client:${this.engineId}] engine process exited unexpectedly (${detail})`);
603
+ }
604
+ this.teardownProcess(detail);
605
+ this.state = "exited";
606
+ }
607
+
608
+ /**
609
+ * 进程死亡 / 失败后的统一清理:镜像整体置死(失效语义 2+3:killed=true 广播后
610
+ * 清空)→ pidfile 删 → 在途请求 engine_crashed(附 stderr 尾)→ run 路由清空。
611
+ */
612
+ private teardownProcess(detail: string): void {
613
+ this.mirror.killAll();
614
+ if (this.pidfileWritten !== undefined) {
615
+ removePidfile(this.pidfileWritten);
616
+ this.pidfileWritten = undefined;
617
+ }
618
+ const err = engineCrashedError(detail, this.stderrTail);
619
+ for (const [, pending] of this.pending) {
620
+ if (pending.timer !== undefined) clearTimeout(pending.timer);
621
+ pending.reject(err);
622
+ }
623
+ this.pending.clear();
624
+ this.runRoutes.clear();
625
+ this.recordRoutes.clear();
626
+ this.lastPartialHandle = undefined;
627
+ if (this.child !== undefined) {
628
+ const child = this.child;
629
+ this.child = undefined;
630
+ // 握手失败路径(版本越界 / 握手超时 / 启动僵死)引擎进程仍活着:必须组杀,
631
+ // 否则泄漏为常驻孤儿(crash 重建循环每次 spawn 泄漏一个;测试机实测积累
632
+ // 370+ 假引擎进程)。主动杀路径(killAll 已发信号,intentionalKill=true)与
633
+ // 进程已死路径(onEngineExit,exitCode/signalCode 非空)跳过重复发信号。
634
+ const alreadyDead = child.exitCode !== null || child.signalCode !== null;
635
+ if (!alreadyDead && !this.intentionalKill && child.pid !== undefined) {
636
+ killProcessTree(child.pid, detail, { engineId: this.engineId });
637
+ }
638
+ child.removeAllListeners();
639
+ }
640
+ if (this.state !== "unavailable" && this.state !== "disposed") {
641
+ this.state = "exited";
642
+ }
643
+ }
644
+
645
+ // ── pidfile ──────────────────────────────────────────────────────────────
646
+
647
+ // ── 帧写出口 ────────────────────────────────────────────────────────────
648
+
649
+ private writeFrame(frame: unknown): boolean {
650
+ const stdin = this.child?.stdin;
651
+ if (stdin === undefined || stdin === null || this.child === undefined || this.child.exitCode !== null) {
652
+ return false;
653
+ }
654
+ try {
655
+ stdin.write(`${JSON.stringify(frame)}\n`);
656
+ return true;
657
+ } catch (err) {
658
+ // 写失败 = 进程已死(EPIPE)——调用方按 false 走 engine_crashed 收口,debug 留痕。
659
+ logger.debug(
660
+ `[engine-client:${this.engineId}] stdin write failed: ${err instanceof Error ? err.message : String(err)}`,
661
+ );
662
+ return false;
663
+ }
664
+ }
665
+
666
+ private sendResponse(id: string, result: unknown): boolean {
667
+ return this.writeFrame({ id, result });
668
+ }
669
+
670
+ private appendStderrTail(chunk: string): void {
671
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
672
+ }
673
+ }
674
+
675
+ // ── 内部工具 ─────────────────────────────────────────────────────────────────
676
+
677
+ function delay(ms: number): Promise<void> {
678
+ return new Promise((resolve) => {
679
+ setTimeout(resolve, ms);
680
+ });
681
+ }
682
+
683
+ function truncate(text: string, max: number): string {
684
+ return text.length <= max ? text : `${text.slice(0, max)}...`;
685
+ }
686
+
687
+