@zhushanwen/subagent-core 0.4.0 → 0.6.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 (309) hide show
  1. package/dist/{chunk-4KN4TTG7.js → chunk-43ONBFZX.js} +1 -1
  2. package/dist/{chunk-X7SZ5HLQ.js → chunk-V3VHZ2VX.js} +20 -27
  3. package/dist/execution/engine/engines/zcode/constants.cjs +29 -41
  4. package/dist/execution/engine/engines/zcode/constants.d.cts +65 -53
  5. package/dist/execution/engine/engines/zcode/constants.d.ts +65 -53
  6. package/dist/execution/engine/engines/zcode/constants.js +17 -27
  7. package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
  8. package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
  9. package/dist/execution/engine/engines/zcode/reader.js +2 -2
  10. package/dist/index.cjs +17100 -14082
  11. package/dist/index.d.cts +5336 -4033
  12. package/dist/index.d.ts +5336 -4033
  13. package/dist/index.js +17020 -14055
  14. package/dist/{types-DpUO16pj.d.cts → types-C3fE3R9x.d.cts} +280 -112
  15. package/dist/{types-DpUO16pj.d.ts → types-C3fE3R9x.d.ts} +280 -112
  16. package/dist.bundle/index.cjs +34933 -0
  17. package/package.json +3 -3
  18. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
  19. package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
  20. package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
  21. package/src/__tests__/m2-append-content-probe.test.ts +0 -1
  22. package/src/__tests__/manifest-store.test.ts +10 -9
  23. package/src/__tests__/record-store-cache.test.ts +0 -2
  24. package/src/__tests__/record-store-index.test.ts +1 -2
  25. package/src/__tests__/review-fix-loop-script.test.ts +4 -2
  26. package/src/__tests__/review-fix-loop-utils.test.ts +10 -10
  27. package/src/__tests__/robustness-low-batch1.test.ts +6 -5
  28. package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
  29. package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
  30. package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
  31. package/src/__tests__/session-runner.test.ts +1 -1
  32. package/src/core/__tests__/host-services.test.ts +2 -2
  33. package/src/core/__tests__/logger.test.ts +1 -1
  34. package/src/core/error-message.ts +9 -0
  35. package/src/core/host-services.ts +21 -5
  36. package/src/core/notify-ports.ts +18 -5
  37. package/src/execution/__tests__/agent-profile.test.ts +4 -4
  38. package/src/execution/__tests__/agent-registry.test.ts +370 -0
  39. package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
  40. package/src/execution/__tests__/agents-assembly.test.ts +2 -2
  41. package/src/execution/__tests__/alive-store.test.ts +1 -1
  42. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
  43. package/src/execution/__tests__/chat-engine-routing.test.ts +68 -33
  44. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +4 -4
  45. package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
  46. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  47. package/src/execution/__tests__/collect-coordinator-service.test.ts +327 -0
  48. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  49. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +262 -0
  50. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  51. package/src/execution/__tests__/config.test.ts +1 -1
  52. package/src/execution/__tests__/delivery-methods.test.ts +75 -62
  53. package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
  54. package/src/execution/__tests__/descendant-sweep.test.ts +6 -6
  55. package/src/execution/__tests__/dialog-queue.test.ts +2 -2
  56. package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
  57. package/src/execution/__tests__/epipe-fallback.test.ts +29 -24
  58. package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
  59. package/src/execution/__tests__/execute-nesting.test.ts +34 -188
  60. package/src/execution/__tests__/execution-record.test.ts +217 -60
  61. package/src/execution/__tests__/execution-runtime-face.test.ts +5 -5
  62. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +4 -4
  63. package/src/execution/__tests__/finalize-record.test.ts +173 -17
  64. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  65. package/src/execution/__tests__/gc-timer.test.ts +56 -3
  66. package/src/execution/__tests__/get-record-for-action-restart.test.ts +12 -12
  67. package/src/execution/__tests__/get-state-handshake.test.ts +1 -1
  68. package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
  69. package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
  70. package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
  71. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
  72. package/src/execution/__tests__/keep-alive-no-progress.test.ts +50 -148
  73. package/src/execution/__tests__/kill-all-escalation.test.ts +2 -2
  74. package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
  75. package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
  76. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  77. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +1 -1
  78. package/src/execution/__tests__/model-resolver.test.ts +2 -2
  79. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
  80. package/src/execution/__tests__/nested-visibility.test.ts +2 -0
  81. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  82. package/src/execution/__tests__/notify-ledger.test.ts +1000 -18
  83. package/src/execution/__tests__/output-collector.test.ts +117 -6
  84. package/src/execution/__tests__/pi-invocation.test.ts +36 -4
  85. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  86. package/src/execution/__tests__/record-store.test.ts +10 -9
  87. package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
  88. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
  89. package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
  90. package/src/execution/__tests__/relay-env.test.ts +12 -1
  91. package/src/execution/__tests__/rpc-mode.test.ts +2 -2
  92. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
  93. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +15 -9
  94. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +4 -4
  95. package/src/execution/__tests__/run-spawn-edges.test.ts +17 -15
  96. package/src/execution/__tests__/run-spawn-integration.test.ts +2 -2
  97. package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
  98. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
  99. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +4 -4
  100. package/src/execution/__tests__/service-kill-escalation.test.ts +7 -5
  101. package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
  102. package/src/execution/__tests__/session-file-gc.test.ts +36 -1
  103. package/src/execution/__tests__/session-pending.test.ts +257 -14
  104. package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
  105. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +1 -1
  106. package/src/execution/__tests__/session-runner-close-prune.test.ts +2 -2
  107. package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
  108. package/src/execution/__tests__/session-runner-epipe.test.ts +3 -3
  109. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +5 -4
  110. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
  111. package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
  112. package/src/execution/__tests__/settled-watchdog.test.ts +282 -47
  113. package/src/execution/__tests__/spawn-args.test.ts +11 -10
  114. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
  115. package/src/execution/__tests__/spawn-event-adapter.test.ts +3 -3
  116. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
  117. package/src/execution/__tests__/spawned-children.test.ts +2 -1
  118. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  119. package/src/execution/__tests__/start-sync-model-guard.test.ts +5 -5
  120. package/src/execution/__tests__/stdin-writer.test.ts +6 -4
  121. package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
  122. package/src/execution/__tests__/subagent-actions-core.test.ts +48 -22
  123. package/src/execution/__tests__/subagent-service-message-close.test.ts +29 -26
  124. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +21 -8
  125. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -22
  126. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +30 -29
  127. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +45 -20
  128. package/src/execution/__tests__/subagent-service.test.ts +37 -30
  129. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
  130. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
  131. package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
  132. package/src/execution/__tests__/sync-collect-recovery.test.ts +1117 -0
  133. package/src/execution/__tests__/temp-prompt.test.ts +1 -1
  134. package/src/execution/__tests__/timeout-integration.test.ts +6 -5
  135. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  136. package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
  137. package/src/execution/__tests__/turn-limiter.test.ts +1 -1
  138. package/src/execution/__tests__/ui-request-handler-factory.test.ts +12 -6
  139. package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
  140. package/src/execution/__tests__/ui-request-queue.test.ts +142 -2
  141. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  142. package/src/execution/__tests__/worktree-manager.test.ts +14 -5
  143. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
  144. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  145. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  146. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  147. package/src/execution/agent-registry.ts +74 -30
  148. package/src/execution/agent-result-mapper.ts +3 -0
  149. package/src/execution/cold-resurrect.ts +199 -0
  150. package/src/execution/collect-coordinator.ts +200 -0
  151. package/src/execution/config.ts +77 -5
  152. package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
  153. package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
  154. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  155. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
  156. package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
  157. package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
  158. package/src/execution/engine/__tests__/common/pool-manager.test.ts +213 -53
  159. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
  160. package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
  161. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +22 -112
  162. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +15 -28
  163. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +1 -1
  164. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +12 -12
  165. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +2 -2
  166. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +21 -82
  167. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +4 -7
  168. package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
  169. package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
  170. package/src/execution/engine/__tests__/registry.test.ts +4 -2
  171. package/src/execution/engine/__tests__/routing.test.ts +115 -6
  172. package/src/execution/engine/common/capability-gate.ts +92 -0
  173. package/src/execution/engine/common/journal-wiring.ts +97 -0
  174. package/src/execution/engine/common/kill-chain.ts +42 -10
  175. package/src/execution/engine/common/nesting-guard.ts +66 -0
  176. package/src/execution/engine/common/persona-router.ts +18 -8
  177. package/src/execution/engine/common/pool-manager.ts +332 -78
  178. package/src/execution/engine/common/session-view-service.ts +506 -0
  179. package/src/execution/engine/common/session-view-types.ts +125 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
  181. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
  182. package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
  183. package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +1 -1
  184. package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
  185. package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
  186. package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
  187. package/src/execution/engine/engines/pi/reader.ts +11 -0
  188. package/src/execution/engine/engines/pi/registration.ts +4 -2
  189. package/src/execution/{session-runner.ts → engine/engines/pi/session-runner.ts} +617 -412
  190. package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
  191. package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
  192. package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
  193. package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
  194. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +54 -27
  195. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
  196. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +13 -12
  197. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
  198. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
  199. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
  200. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
  201. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +20 -8
  202. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +77 -110
  203. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +26 -320
  204. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
  205. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
  206. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
  207. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
  208. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +48 -148
  209. package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
  210. package/src/execution/engine/engines/zcode/connection.ts +24 -11
  211. package/src/execution/engine/engines/zcode/constants.ts +75 -71
  212. package/src/execution/engine/engines/zcode/golden-sample.ts +8 -46
  213. package/src/execution/engine/engines/zcode/parser.ts +12 -346
  214. package/src/execution/engine/engines/zcode/preparer.ts +12 -143
  215. package/src/execution/engine/engines/zcode/session-channel.ts +351 -39
  216. package/src/execution/engine/engines/zcode/zcode-engine.ts +665 -760
  217. package/src/execution/engine/host-task-spec.ts +32 -36
  218. package/src/execution/engine/model-validation.ts +177 -0
  219. package/src/execution/engine/port.ts +24 -6
  220. package/src/execution/engine/routing.ts +75 -1
  221. package/src/execution/engine/types.ts +30 -103
  222. package/src/execution/execution-record.ts +229 -144
  223. package/src/execution/finalize-record.ts +141 -99
  224. package/src/execution/idle-gc.ts +28 -0
  225. package/src/execution/lifecycle-predicates.ts +1 -1
  226. package/src/execution/notifier.ts +234 -25
  227. package/src/execution/notify-host.ts +232 -0
  228. package/src/execution/record-entry.ts +16 -0
  229. package/src/execution/record-store.ts +280 -103
  230. package/src/execution/round-settlement.ts +93 -0
  231. package/src/execution/session-file-gc.ts +96 -62
  232. package/src/execution/session-reconstructor.ts +256 -143
  233. package/src/execution/sessions-index.ts +117 -47
  234. package/src/execution/settled-watchdog.ts +264 -52
  235. package/src/execution/subagent-actions-core.ts +84 -28
  236. package/src/execution/subagent-service.ts +1225 -964
  237. package/src/execution/subprocess-agent-runner.ts +144 -97
  238. package/src/execution/sync-rebuild.ts +87 -0
  239. package/src/execution/types.ts +72 -0
  240. package/src/execution/ui-request-handler-factory.ts +102 -74
  241. package/src/execution/ui-request-queue.ts +50 -19
  242. package/src/index.ts +442 -316
  243. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +1 -1
  244. package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
  245. package/src/orchestration/__tests__/args-validator.test.ts +14 -0
  246. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  247. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +1 -1
  248. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +11 -9
  249. package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
  250. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  251. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  252. package/src/orchestration/__tests__/file-run-store.test.ts +7 -6
  253. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +2 -2
  254. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +1 -1
  255. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
  256. package/src/orchestration/__tests__/lifecycle.test.ts +7 -5
  257. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
  258. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  259. package/src/orchestration/__tests__/run-snapshot.test.ts +8 -5
  260. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  261. package/src/orchestration/__tests__/script-lint.test.ts +17 -0
  262. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  263. package/src/orchestration/__tests__/test-mocks.ts +2 -0
  264. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
  265. package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
  266. package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +27 -19
  267. package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
  268. package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
  269. package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
  270. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +1 -1
  271. package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
  272. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  273. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
  274. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  275. package/src/orchestration/agent-opts-resolver.ts +1 -1
  276. package/src/orchestration/args-validator.ts +53 -31
  277. package/src/orchestration/execute-agent-call.ts +19 -73
  278. package/src/orchestration/launcher.ts +128 -81
  279. package/src/orchestration/lifecycle.ts +43 -45
  280. package/src/orchestration/models/ports.ts +4 -4
  281. package/src/orchestration/models/run-runtime.ts +2 -2
  282. package/src/orchestration/models/trace.ts +1 -1
  283. package/src/orchestration/models/types.ts +111 -22
  284. package/src/orchestration/models/workflow-run.ts +1 -1
  285. package/src/orchestration/script-lint.ts +91 -58
  286. package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +157 -150
  287. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  288. package/src/shared/__tests__/meta-parser.test.ts +151 -0
  289. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
  290. package/src/shared/__tests__/resource-discovery.test.ts +258 -215
  291. package/src/shared/atomic-write.ts +12 -9
  292. package/src/shared/meta-parser.ts +158 -84
  293. package/src/shared/model-ref.ts +15 -2
  294. package/src/shared/resource-discovery.ts +9 -183
  295. package/src/shared/schema-jsonify.ts +1 -1
  296. package/workflows/review-fix-loop-utils.cjs +151 -89
  297. package/workflows/review-fix-loop-utils.d.cts +287 -0
  298. package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
  299. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
  300. package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
  301. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
  302. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
  303. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +0 -387
  304. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -710
  305. package/src/execution/engine/engines/zcode/appserver-home.ts +0 -442
  306. package/src/execution/engine/engines/zcode/appserver-probe.ts +0 -141
  307. package/src/execution/engine/engines/zcode/launcher.ts +0 -161
  308. package/src/execution/execute-options-mapper.ts +0 -115
  309. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
@@ -1,4 +1,4 @@
1
- // src/core/session-runner.ts
1
+ // src/execution/engine/engines/pi/session-runner.ts
2
2
  //
3
3
  // spawn pi --mode rpc 子进程执行 session 的编排器。零 mode 感知。
4
4
  //
@@ -14,56 +14,60 @@ import {
14
14
  } from "node:child_process";
15
15
  import * as fs from "node:fs";
16
16
 
17
- import { getLogger } from "../core/logger.ts";
18
- import { bestEffort } from "./best-effort.ts";
19
- import { disposeEngines } from "./engine/registry.ts";
20
- import { armIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "./lifecycle-manager.ts";
17
+ import { getLogger } from "../../../../core/logger.ts";
18
+ import { bestEffort } from "../../../best-effort.ts";
19
+ import { disposeEngines } from "../../registry.ts";
20
+ import { armIdleTimer, DEFAULT_IDLE_TIMEOUT_MS } from "../../../lifecycle-manager.ts";
21
21
  import {
22
22
  listActivePendingFromSessionFile,
23
23
  prunePendingCursor,
24
24
  readActivePendingFromSessionFile,
25
25
  type ActivePendingResult,
26
- } from "./session-pending.ts";
26
+ } from "../../../session-pending.ts";
27
+ import { killChain } from "../../common/kill-chain.ts";
27
28
 
28
- import type { ExtensionMode } from "./host-mode.ts";
29
+ import type { ExtensionMode } from "../../../host-mode.ts";
29
30
 
30
31
  import { type MirrorFlags, mirrorMainProcessFlags } from "./argv-mirror.ts";
31
- import { isProcessAlive, readAliveMarker, writeAliveMarker } from "./alive-store.ts";
32
- import { type DialogGlobalQueue, type UiRequestHandler } from "./dialog-queue.ts";
33
- import { updateFromEvent } from "./execution-record.ts";
32
+ import { isProcessAlive, readAliveMarker, writeAliveMarker } from "../../../alive-store.ts";
33
+ import { type DialogGlobalQueue, type UiRequestHandler } from "../../../dialog-queue.ts";
34
+ import { updateFromEvent } from "../../../execution-record.ts";
34
35
  import {
35
36
  type AddGetStateResponseListener,
36
37
  type GetStateResult,
37
38
  performGetStateHandshake,
38
39
  requestGetStateOnce,
39
40
  } from "./get-state-handshake.ts";
40
- import { willRespondToAskUser } from "./host-mode.ts";
41
- import type { AgentConfig, ResolvedModel } from "./model-resolver.ts";
41
+ import { willRespondToAskUser } from "../../../host-mode.ts";
42
+ import type { AgentConfig, ResolvedModel } from "../../../model-resolver.ts";
42
43
  import { collectResult } from "./output-collector.ts";
43
- import { getSubagentSessionDir } from "./path-encoding.ts";
44
+ import { getSubagentSessionDir } from "../../../path-encoding.ts";
44
45
  import { getPiInvocation } from "./pi-invocation.ts";
45
- import { isRelayActive, RELAY_ENV_RECORD_ID, RELAY_ENV_SESSION_ID } from "./relay-env.ts";
46
- import { assertThinkingLevel, type ThinkingLevel } from "../shared/model-ref";
46
+ import { isRelayActive, RELAY_ENV_RECORD_ID, RELAY_ENV_SESSION_ID } from "../../../relay-env.ts";
47
+ import { assertThinkingLevel, type ThinkingLevel } from "../../../../shared/model-ref";
47
48
  import {
48
49
  SCHEMA_ENV_MAX_BYTES,
49
50
  SCHEMA_ENV_VAR,
50
51
  schemaEnvByteLength,
51
- } from "../shared/schema-env.ts";
52
- import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
52
+ } from "../../../../shared/schema-env.ts";
53
+ import { assertSafeTimerDelay } from "../../../../shared/timer-delay.ts";
53
54
  import {
54
- armSettledWatchdog,
55
+ armMidRoundNoProgress,
55
56
  disarmSettledWatchdog,
56
- SETTLED_WATCHDOG_TIMEOUT_MS,
57
- } from "./settled-watchdog.ts";
58
- import { MAX_FORK_DEPTH } from "./session-context-resolver.ts";
57
+ handoverMidRoundToSettled,
58
+ refreshMidRoundNoProgress,
59
+ type SettledWatchdogFireInfo,
60
+ } from "../../../settled-watchdog.ts";
61
+ import { MAX_FORK_DEPTH } from "../../../session-context-resolver.ts";
59
62
  import { EPIPE_FAILURE_THRESHOLD, recordEpipeFailure, sendPromptCommand } from "./stdin-writer.ts";
60
63
  import {
61
64
  deriveSessionFilePath,
62
65
  findSessionFileByHeaderId,
63
66
  parseSpawnLine,
67
+ type ParsedSpawnLine,
64
68
  type SpawnSessionHeader,
65
69
  } from "./spawn-event-adapter.ts";
66
- import type { SubagentStream } from "./stream-sink.ts";
70
+ import type { SubagentStream } from "../../../stream-sink.ts";
67
71
  import {
68
72
  cleanupTempPrompt,
69
73
  writePromptToTempFile,
@@ -74,9 +78,10 @@ import type {
74
78
  ExecutionRecord,
75
79
  SdkEvent,
76
80
  WorktreeHandle,
77
- } from "./types.ts";
81
+ } from "../../../types.ts";
78
82
  import { createTurnLimiter, WRAP_UP_HINT } from "./turn-limiter.ts";
79
- import { createUiRequestQueue } from "./ui-request-queue.ts";
83
+ import { createUiRequestQueue } from "../../../ui-request-queue.ts";
84
+ import { toErrorMessage } from "../../../../core/error-message.ts";
80
85
 
81
86
  const logger = getLogger("subagents");
82
87
 
@@ -136,6 +141,28 @@ export function mapAssistantMessageDelta(
136
141
  return null;
137
142
  }
138
143
 
144
+ /**
145
+ * [圈复杂度门禁提取] tool_execution_end 的 args 回填(自 createSpawnEventHandlers 的
146
+ * switch case 体迁出的纯函数,行为逐字节保持):tool_end 可能缺 args,用 tool_start
147
+ * 寄存进 pendingTools 的 args 回填,命中后即消费(delete)。
148
+ *
149
+ * 与 mapAssistantMessageDelta 同域(SdkEvent 翻译纯函数),提取为模块级便于单测。
150
+ */
151
+ function resolveToolEndArgs(
152
+ raw: SdkEvent,
153
+ pendingTools: Map<string, { toolName: string; args?: unknown }>,
154
+ ): unknown {
155
+ let args = raw.args;
156
+ if (raw.toolCallId) {
157
+ const pending = pendingTools.get(raw.toolCallId);
158
+ if (pending) {
159
+ if (args === undefined) args = pending.args;
160
+ pendingTools.delete(raw.toolCallId);
161
+ }
162
+ }
163
+ return args;
164
+ }
165
+
139
166
  // ============================================================
140
167
  // 常量
141
168
  // ============================================================
@@ -354,7 +381,7 @@ const STDERR_MAX_CHARS = 65_536;
354
381
  * 抽叶子的目的:跨包契约测试从 session-runner import 会拖入整条 spawn/pi SDK 依赖树,
355
382
  * 叶子模块提供稳定 import 点(导出名与值不变)。
356
383
  */
357
- export { SCHEMA_ENV_MAX_BYTES, SCHEMA_ENV_VAR };
384
+ export { SCHEMA_ENV_MAX_BYTES };
358
385
 
359
386
  // ============================================================
360
387
  // W4: ask_user RPC 系统提示词
@@ -396,7 +423,7 @@ The \`ask_user\` tool is available in this session. When you call \`ask_user\`,
396
423
  * 此提示在 worktree 模式下注入,明确告知子 agent:cwd 是含完整项目代码的 git worktree,
397
424
  * 直接在此工作即可,不要 cd 别处找"真正的项目"。
398
425
  */
399
- export const WORKTREE_GUIDANCE_PROMPT = `
426
+ const WORKTREE_GUIDANCE_PROMPT = `
400
427
  ## Working Directory Is a Git Worktree
401
428
 
402
429
  Your working directory (the "Working directory" in the environment block above) is a **dedicated git worktree** — an isolated checkout of the repository at HEAD, NOT a temporary sandbox. It contains the **complete project source code**.
@@ -488,7 +515,7 @@ export function killAllSpawnedChildren(signal: NodeJS.Signals = "SIGTERM"): numb
488
515
  // debug 级留诊断线索即可,不刷 info/warn
489
516
  logger.debug(
490
517
  `[session-runner] killAllSpawnedChildren: kill failed (best-effort continue): ${
491
- err instanceof Error ? err.message : String(err)
518
+ toErrorMessage(err)
492
519
  }`,
493
520
  );
494
521
  }
@@ -520,8 +547,8 @@ export function getChildByRecord(recordId: string): ChildProcess | undefined {
520
547
  * 背景:spawnedChildren 是 Map<recordId, ChildProcess>,resume spawn 会 set 覆盖旧句柄。
521
548
  * 旧 child 的 close/error 事件异步到达(kill 后 close 回调可能晚数 tick),若 close/error
522
549
  * handler 无条件 delete(record.id),会误删 resume spawn 刚注册的**新** child——
523
- * 时序:idle timer 对旧 child SIGTERM(killed=true 但 close 未到)→ deliverMessage 判
524
- * child.killed 走冷路径 resumeRound → spawn 新 child set 覆盖 → 旧 child close 此刻到达 →
550
+ * 时序:idle timer 对旧 child SIGTERM(killed=true 但 close 未到)→ 投递方(PiEngine
551
+ * deliverPrompt)判 child.killed 走冷路径续轮 → spawn 新 child set 覆盖 → 旧 child close 此刻到达 →
525
552
  * 误删新注册。后果:新活进程脱离记账(killAllSpawnedChildren 漏杀孤儿 + getChildByRecord
526
553
  * undefined → busy 投递再走冷路径二次 resume → 两进程写同一 session 文件,v4 A-5 注释
527
554
  * 自述的 P7 双写者事故模式)。按句相等守卫:set 覆盖后旧句柄 ≠ Map 当前值,天然跳过。
@@ -608,7 +635,7 @@ function killPidWithEscalation(pid: number, label: string): void {
608
635
  // SIGTERM 发送即失败(进程恰死 / 权限):无需升级,诊断留痕
609
636
  logger.debug(
610
637
  `[session-runner] ${label}: SIGTERM to pid ${pid} failed (best-effort continue): ${
611
- err instanceof Error ? err.message : String(err)
638
+ toErrorMessage(err)
612
639
  }`,
613
640
  );
614
641
  return;
@@ -1020,7 +1047,7 @@ export async function buildEnvBlock(
1020
1047
  // 非 git 目录 / git 不在 PATH 是高频正常路径,debug 级留诊断线索即可,不刷 info/warn
1021
1048
  logger.debug(
1022
1049
  `[session-runner] buildEnvBlock: git branch lookup failed for ${cwd}, fallback to empty: ${
1023
- err instanceof Error ? err.message : String(err)
1050
+ toErrorMessage(err)
1024
1051
  }`,
1025
1052
  );
1026
1053
  branch = "";
@@ -1180,7 +1207,7 @@ function writeAliveMarkerBestEffort(sessionFile: string, pid: number, id: string
1180
1207
  // best-effort:alive marker 失败不影响执行;debug 级留诊断线索即可,不刷 info/warn
1181
1208
  logger.debug(
1182
1209
  `[session-runner] alive marker write failed (best-effort continue): ${
1183
- err instanceof Error ? err.message : String(err)
1210
+ toErrorMessage(err)
1184
1211
  }`,
1185
1212
  );
1186
1213
  }
@@ -1222,6 +1249,13 @@ function touchAliveMarkerForHeartbeat(sessionFile: string | undefined, pid: numb
1222
1249
  // - buildSpawnInvocation:i. pi CLI 参数组装与入口解析
1223
1250
  // - attachStdoutPump:stdout 逐行解析 + get_state 握手状态机 + agent_end keep-alive
1224
1251
  // - waitForChildExit:close/error → exitCode(含统一 cleanup)
1252
+ // - setupFreshChild:spawn 后同步装配(worktree pid 补全 / 记账 / stdio / prompt 命令)
1253
+ // - armChatModeSettledWatchdog:chatMode 首轮 settled 两段守护挂载
1254
+ // - attachAbortAndSpawnWatchdog:abort 监听 + spawn watchdog(返回 onAbort 供注销)
1255
+ // - startGetStateHandshake:get_state 握手 fire-and-forget 启动(含 F2 catch 兜底)
1256
+ // - backfillSessionFileByLookup:收尾 sessionFile 兜底反查(LC-4)
1257
+ // - sweepDescendantsOnChildClose:收尾后代级联补杀(T2-② 后半)
1258
+ // - resolveRunOutcome:成功/失败四分支判定
1225
1259
  // 拆分只移动代码不改行为——runSpawn 导出签名与事件语义不变。
1226
1260
 
1227
1261
  /** runSpawn 各阶段共享的可变状态(原闭包变量收拢,经参数在阶段函数间传递)。 */
@@ -1233,9 +1267,6 @@ interface SpawnRunState {
1233
1267
  proc: ChildProcess | undefined;
1234
1268
  /** watchdog timer(stdout handler 的 agent_end keep-alive 分支重挂,收尾统一 clearTimeout)。 */
1235
1269
  watchdog: NodeJS.Timeout | undefined;
1236
- /** [race-F4] SIGKILL 升级 timer(killChildWithEscalation 挂载;exit 事件自动 clear,
1237
- * 收尾统一 clearTimeout 兑底)。 */
1238
- escalationTimer: NodeJS.Timeout | undefined;
1239
1270
  /** stdout 首行 header(json mode 才有;RPC mode 恒 undefined)——收尾 sessionFile 兜底查找用。 */
1240
1271
  sessionHeader: SpawnSessionHeader | undefined;
1241
1272
  /** get_state 握手结果(RPC mode)——收尾 sessionFile 兜底查找用。 */
@@ -1260,12 +1291,17 @@ interface SpawnRunState {
1260
1291
  */
1261
1292
  sweepDescendantsOnClose: boolean;
1262
1293
  /**
1263
- * [T2-③ / LC-1] chatMode 首轮 settled 等待固定硬上限(timer 句柄记账在
1264
- * settled-watchdog.ts 的 armedTimers Map,按 recordId arm/disarm;双挂载原语,
1265
- * 热路径挂载在 subagent-service deliverMessage,u-t2b 接线)。
1294
+ * [T2-③ / D9 两段式] chatMode 首轮 settled 等待守护(timer 句柄记账在
1295
+ * settled-watchdog.ts 的 armedEntries Map,按 recordId arm/disarm/交棒;双挂载原语,
1296
+ * 热路径挂载在 subagent-service deliverMessage)。中段无进展检测 + 收尾段固定上界
1297
+ * 两段合用本标记。
1298
+ */
1299
+ /**
1300
+ * [T2-③] settled watchdog 已触发及触发段——runSpawn 收尾据此转 failed + 恢复指引
1301
+ * (非正常完成)。undefined = 未触发;mid-round = 中段无进展(30min 静默);
1302
+ * settled = 收尾段固定上界(agent_end 后 600s)。
1266
1303
  */
1267
- /** [T2-③] settled watchdog 已触发——runSpawn 收尾据此转 failed + 恢复指引(非正常完成)。 */
1268
- settledWatchdogFired: boolean;
1304
+ settledWatchdogFired: SettledWatchdogFireInfo | undefined;
1269
1305
  }
1270
1306
 
1271
1307
  /**
@@ -1275,46 +1311,63 @@ interface SpawnRunState {
1275
1311
  * 对外只暴露 handleSdkEvent(stdout 解析出的 SdkEvent 的唯一喂入口)。
1276
1312
  */
1277
1313
 
1314
+ /**
1315
+ * [D3-① 杀链合一] pi 侧杀链参数(现状值,[race-F4]):SIGTERM 优雅窗口 30s——
1316
+ * 超窗未见 exit 视为 SIGTERM 被无视,升级 SIGKILL。秒数单提命名常量再组合 ms:
1317
+ * no-magic-numbers 豁免 const init 单字面量但不豁免乘法表达式中的裸字面量
1318
+ * (对齐上方 watchdog 系列的常量组合写法)。
1319
+ */
1320
+ const PI_KILL_GRACE_SECS = 30;
1321
+ const PI_KILL_GRACE_MS = PI_KILL_GRACE_SECS * MS_PER_SECOND;
1322
+
1323
+ /**
1324
+ * [D3-① 杀链合一 + race-F4] 杀 pi 子进程:发 SIGTERM,30s 优雅窗口后仍存活则 SIGKILL。
1325
+ *
1326
+ * 唯一实现 = common/kill-chain.killChain(本函数只是 pi 参数装配:grace 30s、timer
1327
+ * unref(不阻止主进程退出,dispose 路径 killAllSpawnedChildren 兜底)、SIGKILL 升级
1328
+ * warn 留痕含 record id 与来源)。fire-and-forget:调用方不 await 结果(子进程 close
1329
+ * 事件驱动后续收尾),killChain 内部 exit 事件自动清升级 timer。
1330
+ *
1331
+ * 背景(race-F4):watchdog/limiter/idle timer/abort 触发只发一次 SIGTERM——子进程若
1332
+ * 无视 SIGTERM(卡死在不可中断的 native 调用 / SIGTERM handler 挂死),close 永不触发
1333
+ * → runSpawn 悬挂、background 槽位/worktree/alive marker 泄漏。
1334
+ *
1335
+ * @param source 升级来源标识(warn 日志定位用,如 "spawn watchdog" / "turn limiter")
1336
+ */
1337
+ function killPiChild(child: ChildProcess, recordId: string, source: string): void {
1338
+ void killChain(child, {
1339
+ graceMs: PI_KILL_GRACE_MS,
1340
+ unrefTimers: true,
1341
+ escalationNote: `child ${recordId} (source: ${source})`,
1342
+ });
1343
+ }
1344
+
1278
1345
  /**
1279
1346
  * [race-F4] SIGTERM 后升级 SIGKILL 的等待窗口:30s 未见 exit 视为 SIGTERM 被无视,强杀。
1347
+ * (与上方 PI_KILL_GRACE 同值——runSpawn 内外两杀链入口的现状窗口一致。)
1280
1348
  */
1281
1349
  const SIGKILL_ESCALATION_SECONDS = 30;
1282
1350
  const SIGKILL_ESCALATION_MS = SIGKILL_ESCALATION_SECONDS * MS_PER_SECOND;
1283
1351
 
1284
1352
  /**
1285
- * [race-F4] SIGTERM 并武装 SIGKILL 升级 timer:SIGKILL_ESCALATION_MS 后子进程
1286
- * 仍未退出(exitCode/signalCode null)则 SIGKILL
1353
+ * [race-F4] runSpawn 作用域内 kill 收敛入口(持有 SpawnRunState 的调用方):
1354
+ * SIGTERM 并武装 30s SIGKILL 升级。
1287
1355
  *
1288
1356
  * 背景:watchdog/limiter/idle timer/abort 触发只发一次 SIGTERM——子进程若无视
1289
1357
  * SIGTERM(卡死在不可中断的 native 调用 / SIGTERM handler 挂死),close 永不触发
1290
1358
  * → runSpawn 悬挂、background 槽位/worktree/alive marker 泄漏,旧实现永不回收。
1291
1359
  *
1292
- * - 升级 timer unref(不阻止主进程退出;dispose 路径 killAllSpawnedChildren 兜底)
1293
- * - assertSafeTimerDelay:包内 timer 挂载入口统一校验(常量 30s 恒通过,防未来改
1294
- * 可配置时静默引入 1ms 溢出语义反转)
1295
- * - child exit 事件 clear 升级 timer(自然退出/被 SIGTERM 杀死均不升级)
1296
- * - state.escalationTimer 记录句柄:watchdog re-arm 场景先清旧升级窗口,收尾兜底 clear
1360
+ * [D3-① 合并注] 实现已合一到公共杀链(killPiChild common/kill-chain.killChain,
1361
+ * grace 30s 与原私有链现状逐字节保持)——升级 timer 的记账与清理由 killChain 内部
1362
+ * 承担(exit 事件 settle 自动 clear),原 state.escalationTimer 兜底句柄随旧私有
1363
+ * 链删除(runSpawn 收尾不再 clearTimeout)。本函数保留 state 形态签名:state 参数
1364
+ * 供日志语境(record id),调用点(settled watchdog / keep-alive no-progress /
1365
+ * idle timer / agent_end 处置链)语义不变。
1297
1366
  *
1298
1367
  * @param source 升级来源标识(warn 日志定位用,如 "spawn watchdog" / "turn limiter")
1299
1368
  */
1300
1369
  function killChildWithEscalation(state: SpawnRunState, child: ChildProcess, source: string): void {
1301
- child.kill("SIGTERM");
1302
- if (state.escalationTimer) clearTimeout(state.escalationTimer);
1303
- assertSafeTimerDelay(SIGKILL_ESCALATION_MS, `SIGKILL escalation (${source})`);
1304
- const escalation = setTimeout(
1305
- () => {
1306
- if (child.exitCode === null && child.signalCode === null) {
1307
- logger.warn(
1308
- `[session-runner] child ${state.record.id} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND}s after SIGTERM, escalating to SIGKILL (source: ${source})`,
1309
- );
1310
- child.kill("SIGKILL");
1311
- }
1312
- },
1313
- SIGKILL_ESCALATION_MS,
1314
- );
1315
- escalation.unref();
1316
- child.once("exit", () => clearTimeout(escalation));
1317
- state.escalationTimer = escalation;
1370
+ killPiChild(child, state.record.id, source);
1318
1371
  }
1319
1372
 
1320
1373
  /**
@@ -1508,7 +1561,7 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
1508
1561
  },
1509
1562
  abort: () => {
1510
1563
  // [race-F4] 升级路径:SIGTERM 后 30s 未 exit 则 SIGKILL(挂住子进程永不回收防线)
1511
- if (state.proc) killChildWithEscalation(state, state.proc, "turn limiter abort");
1564
+ if (state.proc) killPiChild(state.proc, record.id, "turn limiter abort");
1512
1565
  },
1513
1566
  });
1514
1567
 
@@ -1538,61 +1591,65 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
1538
1591
  }
1539
1592
  };
1540
1593
 
1541
- const handleSdkEvent = (raw: SdkEvent): void => {
1542
- // [V2 模块 3] agent_settled:真空闲边界(agent_end 之后、post-run 完成后才 emit)。
1543
- // chatMode:arm idle timer(超时 SIGTERM 回收)+ 通知本轮完成(onRoundSettled)。
1544
- // 非 chatMode:忽略(agent_end handler 的一次性 kill 已处理,进程不会活到 agent_settled)。
1545
- if (isAgentSettledEvt(raw)) {
1546
- if (record.chatMode) {
1547
- // [T2-③ / LC-1] settled 到达:本轮等待窗口结束,固定硬上限即清(resolveRun
1548
- // 在本分支同点调用,天然同清)。清除必须先于后续逻辑——idle timer / 回调 /
1549
- // resolve 抛错时 watchdog 已确保撤下,不会误杀下一个正常轮次。
1550
- disarmSettledWatchdog(record.id);
1551
- // [F-R2] 本闭包经 stdout data 回调同步调用(handleSdkEvent ← attachStdoutPump):
1552
- // armIdleTimer → assertSafeTimerDelay fail-fast 的 throw 若逃出回调 = uncaughtException
1553
- // 崩宿主。包 try/catch 降级,错误经 bestEffort("error") 可见但不升级为进程崩溃;
1554
- // 后续 limiter.reset / onRoundSettled / resolveRun 照常执行(本轮完成通知不因 GC
1555
- // timer 故障丢失)。
1556
- // [T4② / PS-4] 降级语义修正:旧降级「不挂 idle timer」保住了「不崩进程」,却丢掉
1557
- // timer 承载的两个下游不变量——isIdle 放行门(hasIdleTimer=false → 轮次完成通知被
1558
- // lifecycle-predicates 吞)与进程回收(进程活着却无 timer 永久泄漏)。现降级改为
1559
- // 「挂 DEFAULT_IDLE_TIMEOUT_MS + warn 留痕」:非配置替换(配置错误已在 spawn 入口
1560
- // fail-fast,见 subagent-service),此处是防御性兜底,兜底必须可见且保住不变量。
1561
- const armIdleTimerOnTimeout = (): void => {
1562
- // onTimeout 复用现有 kill 路径:child.kill("SIGTERM") 触发 close → close handler
1563
- // 统一 cleanup(spawnedChildren.delete / get_stateListeners.clear / resolve)。
1564
- // 与 agent_end handler 现有 SIGTERM 分支一致,不新造 cleanup。
1565
- // [race-F4] 升级:idle timer SIGTERM 后挂住 → 30s 后 SIGKILL。
1566
- const child = getChildByRecord(record.id);
1567
- if (child && !child.killed) killChildWithEscalation(state, child, "idle timer");
1568
- };
1569
- try {
1570
- armIdleTimer(record.id, armIdleTimerOnTimeout, record.idleTimeoutMs);
1571
- } catch (err) {
1572
- bestEffort(err, "armIdleTimer (agent_settled chatMode)", "error");
1573
- try {
1574
- armIdleTimer(record.id, armIdleTimerOnTimeout, DEFAULT_IDLE_TIMEOUT_MS);
1575
- logger.warn(
1576
- `[session-runner] 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`,
1577
- );
1578
- } catch (fallbackErr) {
1579
- // 双重失败(理论上不可达:DEFAULT 恒在安全域内)——退回旧「不挂」语义但留痕。
1580
- bestEffort(fallbackErr, "armIdleTimer fallback (agent_settled chatMode)", "error");
1581
- }
1582
- }
1583
- // [SP-9] chatMode 每轮 reset turn-limiter:新一轮开始(续聊)时,
1584
- // maxTurns/graceTurns 不跨轮累计(续聊本质是无限轮,累计上限违背 G1)。
1585
- // reset steered/aborted 标志 + turnCount 归零,下一轮独立计数。
1586
- limiter.reset();
1587
- record.turnCount = 0;
1588
- ctx.onRoundSettled?.(record);
1589
- // [V2 决策 2] chatMode 首轮:agent_settled = 本轮真空闲,提前 resolve runSpawn
1590
- //(exit code 0,进程仍保活 idle timer armed)。runAndFinalize 拿到 result 后走
1591
- // chatMode 首轮分支(不进 finalize 分流),onRoundSettled 已 notify 主 agent。
1592
- state.resolveRun?.(0);
1594
+ /**
1595
+ * [圈复杂度门禁提取] agent_settled × chatMode 分支整体自 handleSdkEvent 迁入
1596
+ * (只移动代码不改行为)。含 idle timer 挂载降级链 + 本轮完成通知 + 首轮提前 resolve。
1597
+ */
1598
+ const handleAgentSettledForChatMode = (): void => {
1599
+ // [T2-③ / D9 两段式] settled 到达:本轮等待结束,两段守护(中段无进展 +
1600
+ // 收尾段上界)一并即清(resolveRun 在本分支同点调用,天然同清)。清除必须
1601
+ // 先于后续逻辑——idle timer / 回调 / resolve 抛错时 watchdog 已确保撤下,
1602
+ // 不会误杀下一个正常轮次。
1603
+ disarmSettledWatchdog(record.id);
1604
+ // [F-R2] 本闭包经 stdout data 回调同步调用(handleSdkEvent ← attachStdoutPump):
1605
+ // armIdleTimer → assertSafeTimerDelay fail-fast 的 throw 若逃出回调 = uncaughtException
1606
+ // 崩宿主。包 try/catch 降级,错误经 bestEffort("error") 可见但不升级为进程崩溃;
1607
+ // 后续 limiter.reset / onRoundSettled / resolveRun 照常执行(本轮完成通知不因 GC
1608
+ // timer 故障丢失)。
1609
+ // [T4② / PS-4] 降级语义修正:旧降级「不挂 idle timer」保住了「不崩进程」,却丢掉
1610
+ // timer 承载的两个下游不变量——isIdle 放行门(hasIdleTimer=false → 轮次完成通知被
1611
+ // lifecycle-predicates 吞)与进程回收(进程活着却无 timer 永久泄漏)。现降级改为
1612
+ // 「挂 DEFAULT_IDLE_TIMEOUT_MS + warn 留痕」:非配置替换(配置错误已在 spawn 入口
1613
+ // fail-fast,见 subagent-service),此处是防御性兜底,兜底必须可见且保住不变量。
1614
+ const armIdleTimerOnTimeout = (): void => {
1615
+ // onTimeout 复用现有 kill 路径:child.kill("SIGTERM") 触发 close → close handler
1616
+ // 统一 cleanup(spawnedChildren.delete / get_stateListeners.clear / resolve)。
1617
+ // 与 agent_end handler 现有 SIGTERM 分支一致,不新造 cleanup。
1618
+ // [race-F4] 升级:idle timer SIGTERM 后挂住 → 30s 后 SIGKILL。
1619
+ const child = getChildByRecord(record.id);
1620
+ if (child && !child.killed) killChildWithEscalation(state, child, "idle timer");
1621
+ };
1622
+ try {
1623
+ armIdleTimer(record.id, armIdleTimerOnTimeout, record.idleTimeoutMs);
1624
+ } catch (err) {
1625
+ bestEffort(err, "armIdleTimer (agent_settled chatMode)", "error");
1626
+ try {
1627
+ armIdleTimer(record.id, armIdleTimerOnTimeout, DEFAULT_IDLE_TIMEOUT_MS);
1628
+ logger.warn(
1629
+ `[session-runner] 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`,
1630
+ );
1631
+ } catch (fallbackErr) {
1632
+ // 双重失败(理论上不可达:DEFAULT 恒在安全域内)——退回旧「不挂」语义但留痕。
1633
+ bestEffort(fallbackErr, "armIdleTimer fallback (agent_settled chatMode)", "error");
1593
1634
  }
1594
- return;
1595
1635
  }
1636
+ // [SP-9] chatMode 每轮 reset turn-limiter:新一轮开始(续聊)时,
1637
+ // maxTurns/graceTurns 不跨轮累计(续聊本质是无限轮,累计上限违背 G1)。
1638
+ // reset steered/aborted 标志 + turnCount 归零,下一轮独立计数。
1639
+ limiter.reset();
1640
+ record.turnCount = 0;
1641
+ ctx.onRoundSettled?.(record);
1642
+ // [V2 决策 2] chatMode 首轮:agent_settled = 本轮真空闲,提前 resolve runSpawn
1643
+ //(exit code 0,进程仍保活 idle timer armed)。runAndFinalize 拿到 result 后走
1644
+ // chatMode 首轮分支(不进 finalize 分流),onRoundSettled 已 notify 主 agent。
1645
+ state.resolveRun?.(0);
1646
+ };
1647
+
1648
+ /**
1649
+ * [圈复杂度门禁提取] SdkEvent.type switch 分派整体自 handleSdkEvent 迁入
1650
+ * (只移动代码不改行为)。tool_end 的 args 回填另有模块级 resolveToolEndArgs。
1651
+ */
1652
+ const dispatchSdkEventByType = (raw: SdkEvent): void => {
1596
1653
  switch (raw.type) {
1597
1654
  case "tool_execution_start": {
1598
1655
  const toolName = raw.toolName ?? "";
@@ -1604,15 +1661,13 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
1604
1661
  }
1605
1662
  case "tool_execution_end": {
1606
1663
  const toolName = raw.toolName ?? "";
1607
- let args = raw.args;
1608
- if (raw.toolCallId) {
1609
- const pending = pendingTools.get(raw.toolCallId);
1610
- if (pending) {
1611
- if (args === undefined) args = pending.args;
1612
- pendingTools.delete(raw.toolCallId);
1613
- }
1614
- }
1615
- agentEvent({ type: "tool_end", toolName, args, result: raw.result, isError: raw.isError });
1664
+ agentEvent({
1665
+ type: "tool_end",
1666
+ toolName,
1667
+ args: resolveToolEndArgs(raw, pendingTools),
1668
+ result: raw.result,
1669
+ isError: raw.isError,
1670
+ });
1616
1671
  return;
1617
1672
  }
1618
1673
  case "message_update": {
@@ -1641,6 +1696,21 @@ function createSpawnEventHandlers(state: SpawnRunState): (raw: SdkEvent) => void
1641
1696
  }
1642
1697
  };
1643
1698
 
1699
+ const handleSdkEvent = (raw: SdkEvent): void => {
1700
+ // [T2-③ / D9 两段式] 有效协议事件行刷新中段无进展计时(本函数是 stdout pump
1701
+ // 解析出的合法 SdkEvent 的唯一喂入口——LC-9 invalid 行不进入,天然不刷新)。
1702
+ // 未挂载(非 chatMode / env 关闭)或已交棒(收尾段不刷新)时幂等 no-op。
1703
+ refreshMidRoundNoProgress(record.id);
1704
+ // [V2 模块 3] agent_settled:真空闲边界(agent_end 之后、post-run 完成后才 emit)。
1705
+ // chatMode:arm idle timer(超时 SIGTERM 回收)+ 通知本轮完成(onRoundSettled)。
1706
+ // 非 chatMode:忽略(agent_end handler 的一次性 kill 已处理,进程不会活到 agent_settled)。
1707
+ if (isAgentSettledEvt(raw)) {
1708
+ if (record.chatMode) handleAgentSettledForChatMode();
1709
+ return;
1710
+ }
1711
+ dispatchSdkEventByType(raw);
1712
+ };
1713
+
1644
1714
  return handleSdkEvent;
1645
1715
  }
1646
1716
 
@@ -2069,6 +2139,106 @@ function attachStdoutPump(
2069
2139
  settleHandshakeNow();
2070
2140
  };
2071
2141
 
2142
+ /**
2143
+ * [圈复杂度门禁提取] header 行处理(自 data handler 的 kind 分支整体迁入,
2144
+ * 只移动代码不改行为):sessionFile 推导回填 + alive marker + worktree pid 补全
2145
+ * + header 加速路径握手。
2146
+ */
2147
+ const handleHeaderLine = (parsed: Extract<ParsedSpawnLine, { kind: "header" }>): void => {
2148
+ state.sessionHeader = parsed.header;
2149
+ // 回填 record.sessionFile(deriveSessionFilePath 推导路径)
2150
+ record.sessionFile = deriveSessionFilePath(parsed.header, sessionDir);
2151
+ // [持久化 C] alive marker:running 期间崩溃恢复用。子进程 pid + session id。
2152
+ // 与 in-process 逻辑对齐(记 sessionFile + pid),改为子进程 pid。
2153
+ if (record.sessionFile && child.pid) {
2154
+ writeAliveMarkerBestEffort(record.sessionFile, child.pid, parsed.header.id);
2155
+ }
2156
+ // [全局注册表] worktree 模式:补全注册表条目的 pid。
2157
+ // create 时 pid 未知写 0 占位,此处拿到 child.pid 后回调 WorktreeManager.registerPid。
2158
+ // 取代旧的 .session mapping sidecar——注册表是 reaper 的唯一数据源。
2159
+ // [D5a] fire-and-forget:回调内部走跨进程锁(毫秒级),且实现方保证不
2160
+ // reject(锁降级兜底);stdout data 回调是同步上下文,不 await。
2161
+ if (opts.worktree && child.pid) {
2162
+ // 透传 record.sessionFile:填入 registry entry(reaper 据 pid 死活判孤儿),
2163
+ // first header 时 sessionFile 已回填(deriveSessionFilePath 在本分支上方)。
2164
+ try {
2165
+ void ctx.onWorktreePid?.(opts.worktree.branch, child.pid, record.sessionFile);
2166
+ } catch (err) {
2167
+ // 同步段异常(回调本身 throw)不阻断 stdout 解析;锁内错误由回调内部 warn。
2168
+ bestEffort(err, "onWorktreePid callback (first header)");
2169
+ }
2170
+ }
2171
+ // FR-4 加速路径:header 到达即 finishHandshake(header 已提供 sessionId,
2172
+ // 足以推导 sessionFile + 兜底查找,无需等 get_state response)。
2173
+ // [#25] buildSpawnArgs 固定 --mode rpc,RPC mode 不发 header——此分支当前不触发,
2174
+ // 仅为未来 mode 回切(如 json mode 调试)保留:届时 header 先到可省去 get_state 握手等待。
2175
+ if (settleHandshake) {
2176
+ finishHandshake({
2177
+ ...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
2178
+ sessionId: parsed.header.id,
2179
+ });
2180
+ }
2181
+ };
2182
+
2183
+ /**
2184
+ * [圈复杂度门禁提取] agent_end 路由(自 data handler 的 event 分支迁入,只移动
2185
+ * 代码不改行为)。返回 true = chatMode 交棒形态——调用方据此跳过本行的
2186
+ * handleSdkEvent 喂入。原实现用 continue(跳过当前行剩余、继续 for(line) 后续行,
2187
+ * 不跳出整个 flush),提取后 return 于 for 循环体内同构。
2188
+ */
2189
+ const routeAgentEnd = (evt: { type: "agent_end"; willRetry?: boolean }): boolean => {
2190
+ if (evt.willRetry) {
2191
+ // agent 会重试,不能 kill。
2192
+ return false;
2193
+ }
2194
+ if (record.chatMode) {
2195
+ // [V2 决策 1] chatMode:对话模式进程不因轮次死。agent_end 不 kill、不 MF-3/MF-4。
2196
+ // 等待 agent_settled(真空闲信号)arm idle timer + notify(onRoundSettled)。
2197
+ // [T2-③ / D9 两段式] agent_end 到达 = 工作段结束:中段无进展计时让位收尾段
2198
+ // 固定上界(清中段、已过时间不继承,从 agent_end 起独立计时 600s;未挂载 /
2199
+ // 已交棒幂等 no-op——首轮与热路径轮共用本 pump 接线点)。
2200
+ handoverMidRoundToSettled(record.id);
2201
+ return true;
2202
+ }
2203
+ // [recursive-orchestration] 条件 kill:读子进程 session 文件算活跃后代
2204
+ // (pending:register − unregister 差集)。有活跃后代(background subagent /
2205
+ // workflow)→ 保持进程 idle,等后代完成时 notifier triggerTurn steer 唤醒;
2206
+ // 无 → 正常完成,kill 触发 close → runSpawn resolve。
2207
+ //
2208
+ // [T1/RC-1] 处置决策异步化(fire-and-forget):sessionFile 缺失(RC-1 握手失败
2209
+ // 形态)时现场惰性 get_state 回补后再判定,见 runAgentEndDisposition。原同步
2210
+ // 三分支逐行迁入该函数;异步化后决策最晚 1s(回补超时预算)落地,期间子进程
2211
+ // 仍在原 watchdog 保护下,kill 延迟无语义影响。
2212
+ void runAgentEndDisposition(state, child, sessionDir, registerGetStateListener);
2213
+ return false;
2214
+ };
2215
+
2216
+ /**
2217
+ * [圈复杂度门禁提取] 事件行处理(自 data handler 的 event 分支迁入):
2218
+ * agent_end 路由(keep-alive / chatMode 交棒 / 条件 kill)+ 其余事件喂 handleSdkEvent。
2219
+ */
2220
+ const handleEventLine = (parsed: Extract<ParsedSpawnLine, { kind: "event" }>): void => {
2221
+ // agent_end(willRetry=false)= agent 自然完成。rpc mode 子进程不自动退出
2222
+ //(runRpcMode 末尾 return new Promise(() => {}) 长驻等命令),需主动 kill
2223
+ // 触发 close → runSpawn resolve。willRetry=true 时 agent 会重试,不能 kill。
2224
+ if (isAgentEndEvt(parsed.event) && routeAgentEnd(parsed.event)) return;
2225
+ if (isSdkEvent(parsed.event)) handleSdkEvent(parsed.event);
2226
+ };
2227
+
2228
+ /**
2229
+ * [圈复杂度门禁提取] RPC response 行处理(自 data handler 的 response 分支迁入):
2230
+ * FR-4 get_state response 按 id 匹配 resolver。
2231
+ */
2232
+ const handleResponseLine = (parsed: Extract<ParsedSpawnLine, { kind: "response" }>): void => {
2233
+ if (parsed.command === "get_state" && parsed.success && parsed.id) {
2234
+ const resolver = get_stateListeners.get(parsed.id);
2235
+ if (resolver) {
2236
+ get_stateListeners.delete(parsed.id);
2237
+ resolver(parsed.data);
2238
+ }
2239
+ }
2240
+ };
2241
+
2072
2242
  child.stdout.on("data", (data: string) => {
2073
2243
  // [T2-① / P-T2 降级 B] 子进程有输出 = keep-alive 仍有进展迹象:刷新静默计时。
2074
2244
  // 任何 stdout 活动(header / 事件行 / invalid 调试行)都算——keep-alive 的合法性
@@ -2081,81 +2251,18 @@ function attachStdoutPump(
2081
2251
  stdoutBuffer += data;
2082
2252
  const lines = stdoutBuffer.split("\n");
2083
2253
  stdoutBuffer = lines.pop() ?? ""; // 保留最后未完整行
2254
+ // [圈复杂度门禁提取] 各 kind 分支体整体迁入上方 handleHeaderLine /
2255
+ // handleEventLine / handleResponseLine(行为逐字节保持),本 handler 只留
2256
+ // 缓冲切分 + kind 分派。
2084
2257
  for (const line of lines) {
2085
2258
  const parsed = parseSpawnLine(line);
2086
2259
  if (!parsed) continue;
2087
2260
  if (parsed.kind === "header") {
2088
- state.sessionHeader = parsed.header;
2089
- // 回填 record.sessionFile(deriveSessionFilePath 推导路径)
2090
- record.sessionFile = deriveSessionFilePath(parsed.header, sessionDir);
2091
- // [持久化 C] alive marker:running 期间崩溃恢复用。子进程 pid + session id。
2092
- // 与 in-process 逻辑对齐(记 sessionFile + pid),改为子进程 pid。
2093
- if (record.sessionFile && child.pid) {
2094
- writeAliveMarkerBestEffort(record.sessionFile, child.pid, parsed.header.id);
2095
- }
2096
- // [全局注册表] worktree 模式:补全注册表条目的 pid。
2097
- // create 时 pid 未知写 0 占位,此处拿到 child.pid 后回调 WorktreeManager.registerPid。
2098
- // 取代旧的 .session mapping sidecar——注册表是 reaper 的唯一数据源。
2099
- // [D5a] fire-and-forget:回调内部走跨进程锁(毫秒级),且实现方保证不
2100
- // reject(锁降级兜底);stdout data 回调是同步上下文,不 await。
2101
- if (opts.worktree && child.pid) {
2102
- // 透传 record.sessionFile:填入 registry entry(reaper 据 pid 死活判孤儿),
2103
- // first header 时 sessionFile 已回填(deriveSessionFilePath 在本分支上方)。
2104
- try {
2105
- void ctx.onWorktreePid?.(opts.worktree.branch, child.pid, record.sessionFile);
2106
- } catch (err) {
2107
- // 同步段异常(回调本身 throw)不阻断 stdout 解析;锁内错误由回调内部 warn。
2108
- bestEffort(err, "onWorktreePid callback (first header)");
2109
- }
2110
- }
2111
- // FR-4 加速路径:header 到达即 finishHandshake(header 已提供 sessionId,
2112
- // 足以推导 sessionFile + 兜底查找,无需等 get_state response)。
2113
- // [#25] buildSpawnArgs 固定 --mode rpc,RPC mode 不发 header——此分支当前不触发,
2114
- // 仅为未来 mode 回切(如 json mode 调试)保留:届时 header 先到可省去 get_state 握手等待。
2115
- if (settleHandshake) {
2116
- finishHandshake({
2117
- ...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
2118
- sessionId: parsed.header.id,
2119
- });
2120
- }
2261
+ handleHeaderLine(parsed);
2121
2262
  } else if (parsed.kind === "event") {
2122
- const evt = parsed.event;
2123
- // agent_end(willRetry=false)= agent 自然完成。rpc mode 子进程不自动退出
2124
- //(runRpcMode 末尾 return new Promise(() => {}) 长驻等命令),需主动 kill
2125
- // 触发 close → runSpawn resolve。willRetry=true 时 agent 会重试,不能 kill。
2126
- if (isAgentEndEvt(evt)) {
2127
- if (evt.willRetry) {
2128
- // agent 会重试,不能 kill。
2129
- } else if (record.chatMode) {
2130
- // [V2 决策 1] chatMode:对话模式进程不因轮次死。agent_end 不 kill、不 MF-3/MF-4。
2131
- // 等待 agent_settled(真空闲信号)arm idle timer + notify(onRoundSettled)。
2132
- // 用 continue 而非 return:return 会跳出 stdout data handler 的 for(line) 循环,
2133
- // 丢弃同一 flush 内 agent_end 之后的事件(如紧随的 agent_settled)。continue 只
2134
- // 跳过当前行剩余(handleSdkEvent 对 agent_end 是 no-op),继续处理后续行。
2135
- continue;
2136
- } else {
2137
- // [recursive-orchestration] 条件 kill:读子进程 session 文件算活跃后代
2138
- // (pending:register − unregister 差集)。有活跃后代(background subagent /
2139
- // workflow)→ 保持进程 idle,等后代完成时 notifier triggerTurn steer 唤醒;
2140
- // 无 → 正常完成,kill 触发 close → runSpawn resolve。
2141
- //
2142
- // [T1/RC-1] 处置决策异步化(fire-and-forget):sessionFile 缺失(RC-1 握手失败
2143
- // 形态)时现场惰性 get_state 回补后再判定,见 runAgentEndDisposition。原同步
2144
- // 三分支逐行迁入该函数;异步化后决策最晚 1s(回补超时预算)落地,期间子进程
2145
- // 仍在原 watchdog 保护下,kill 延迟无语义影响。
2146
- void runAgentEndDisposition(state, child, sessionDir, registerGetStateListener);
2147
- }
2148
- }
2149
- if (isSdkEvent(parsed.event)) handleSdkEvent(parsed.event);
2263
+ handleEventLine(parsed);
2150
2264
  } else if (parsed.kind === "response") {
2151
- // FR-4: RPC response handling — 匹配 get_state 响应
2152
- if (parsed.command === "get_state" && parsed.success && parsed.id) {
2153
- const resolver = get_stateListeners.get(parsed.id);
2154
- if (resolver) {
2155
- get_stateListeners.delete(parsed.id);
2156
- resolver(parsed.data);
2157
- }
2158
- }
2265
+ handleResponseLine(parsed);
2159
2266
  } else if (parsed.kind === "extension_ui_request") {
2160
2267
  // W3: 子进程发 UI 请求(ask_user)。入队 FIFO 串行处理,防止并发询问用户。
2161
2268
  enqueueUiRequest(parsed.id, parsed.request);
@@ -2274,6 +2381,298 @@ function waitForChildExit(
2274
2381
  });
2275
2382
  }
2276
2383
 
2384
+ /**
2385
+ * [圈复杂度门禁提取] spawn 后同步装配阶段(自 runSpawn 迁入,只移动代码不改行为):
2386
+ * worktree pid 同步补全 + spawnedChildren 记账 + record.pid + stdio 编码 +
2387
+ * stdin 异步 error listener + 首条 prompt 命令。顺序敏感(见各段注释),整段迁移。
2388
+ */
2389
+ function setupFreshChild(
2390
+ state: SpawnRunState,
2391
+ child: ChildProcessWithoutNullStreams,
2392
+ task: string,
2393
+ ): void {
2394
+ const { record, opts, ctx } = state;
2395
+ // [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
2396
+ // 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
2397
+ // 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
2398
+ // SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
2399
+ // header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
2400
+ if (opts.worktree && child.pid) {
2401
+ // [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
2402
+ void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
2403
+ }
2404
+ // [C1] track 子进程供 dispose 兜底 kill(sync + background 均注册——sync 无 controller,
2405
+ // abortRunningControllers 跳过它,靠本 Map 兜底)。close/error 后按句守卫移除(已退出无需再 kill)。
2406
+ spawnedChildren.set(record.id, child);
2407
+ // [V2 决策 3] spawn 后 child.pid 同步立即可得,记录到 record 内存(lifecycle-manager
2408
+ // 孤儿扫描用,Step 5 接入持久化)。resume spawn 时此处同样覆盖更新(pid 可能已变)。
2409
+ if (child.pid !== undefined) record.pid = child.pid;
2410
+
2411
+ // [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
2412
+ // 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
2413
+ // 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
2414
+ // SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
2415
+ // header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
2416
+ // [S1] 防御:必须放在 spawnedChildren.add 之后(onWorktreePid 抛错时子进程已被跟踪,
2417
+ // dispose 兜底 kill 不会泄漏),且包 try/catch(补全失败不阻断 spawn 主流程——
2418
+ // 注册表写失败最坏后果是条目停留 pid=0,由 reaper 宽限回收兜底)。
2419
+ if (opts.worktree && child.pid) {
2420
+ try {
2421
+ // [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
2422
+ void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
2423
+ } catch (err) {
2424
+ logger.warn("[worktree] worktree pid registration failed (defensive)", {
2425
+ branch: opts.worktree.branch,
2426
+ pid: child.pid,
2427
+ err: toErrorMessage(err),
2428
+ });
2429
+ }
2430
+ }
2431
+
2432
+ // stdout/stderr 用 utf8 编码:stream 自动按字符边界切分,避免多字节
2433
+ // UTF-8(CJK/emoji)跨 chunk 时 toString() 产生 U+FFFD 替换符导致 JSON.parse 失败。
2434
+ // [m2] 先 setEncoding 再注册 signal listener/watchdog:若 setEncoding 抛错,try/finally
2435
+ //(下方)只清理 tempPromptFile,watchdog/signal listener 尚未注册则无需清理——避免泄漏。
2436
+ child.stdout.setEncoding("utf8");
2437
+ child.stderr.setEncoding("utf8");
2438
+
2439
+ // [v4 A-1] 异步 stdin 'error' listener。writeStdinLine 的 try/catch 只覆盖同步 write 抛错;
2440
+ // 子进程退出后内核回写 EPIPE 会以异步 stream 'error' event 到达,若无 listener 会让 Node
2441
+ // 抛 unhandled 'error' 崩主进程(P1)。必须在首次 stdin 写入(下方 sendPromptCommand)前注册。
2442
+ // handler 行为:①移出 spawnedChildren 标记 dead(与 child.on('error') 同模式);
2443
+ // ②recordEpipeFailure 合并同步/异步计数;③logger.warn 记录一次。
2444
+ // [v4 A-1 裁决] handler 内**不 throw**——stream 'error' listener 内 throw 会经 Node 内部
2445
+ // emit() 传播为 uncaughtException 崩主进程,违背 A-1 防崩核心目标。达阈值的 throw 留给
2446
+ // 同步路径(PiEngine.deliverPrompt 的 catch 合并计数达 EPIPE_FAILURE_THRESHOLD 时同步 throw,不崩)。
2447
+ // async handler 只移句柄 + 计数 + warn;进程已 dead(移句柄),下次投递检测
2448
+ // dead 走冷路径,冷路径 write EPIPE 同步计数达阈值同步 throw——防死循环且不崩。
2449
+ child.stdin.on("error", (err: Error) => {
2450
+ // [M4] 按值守卫:resume spawn 已覆盖注册时不误删新 child(见 removeChildRegistration)
2451
+ removeChildRegistration(record.id, child);
2452
+ const count = recordEpipeFailure(record.id);
2453
+ logger.warn(`[subagents] async stdin error for ${record.id}`, {
2454
+ detail: err.message,
2455
+ epipeCount: count,
2456
+ threshold: EPIPE_FAILURE_THRESHOLD,
2457
+ hint:
2458
+ count >= EPIPE_FAILURE_THRESHOLD
2459
+ ? "sync path will throw on next write EPIPE"
2460
+ : undefined,
2461
+ });
2462
+ });
2463
+
2464
+ // 喂 prompt 命令驱动子进程开始处理 task。pi runRpcMode 只消费 stdin RpcCommand,
2465
+ // 不读 positional arg;必须在 spawn 后主动写,否则子进程阻塞、totalTokens 恒 0。
2466
+ // 时机安全:pipe 内核缓冲不丢;pi 在 rebindSession 后才挂 stdin reader。
2467
+ sendPromptCommand(child, task);
2468
+ }
2469
+
2470
+ /**
2471
+ * [圈复杂度门禁提取] chatMode 首轮 settled 等待守护挂载(自 runSpawn 迁入)。
2472
+ */
2473
+ function armChatModeSettledWatchdog(state: SpawnRunState, child: ChildProcess): void {
2474
+ const { record } = state;
2475
+ // [T2-③ / D9 两段式] chatMode 首轮 settled 等待守护(双挂载原语之首轮调用点;
2476
+ // 热路径调用点在 subagent-service deliverMessage)。prompt 发出后挂**中段**无进展
2477
+ // 检测(有效协议事件行刷新,连续静默 30min 判 wedged);agent_end 到达时经 stdout
2478
+ // pump 交棒**收尾段**固定上界(agent_end 后 600s,P-T2c 定案);settled 到达
2479
+ //(handleSdkEvent)/ close(waitForChildExit)/ resolveRun 任一发生即清——两段
2480
+ // 合起来覆盖 LC-1 三 wedged 形态(agent_end 永不到达 / 收尾卡死 / settled 行丢失)。
2481
+ // 到期处置(两段同构):kill 层主 + settledWatchdogFired 段信息(收尾据此转 failed
2482
+ // + 恢复指引,见 resolveRunOutcome),与被信号终止视为正常完成的既有语义区分。
2483
+ if (record.chatMode) {
2484
+ armMidRoundNoProgress(record.id, {
2485
+ onMidTimeout: (fire) => {
2486
+ logger.warn(
2487
+ `[session-runner] settled watchdog (mid-round) fired for ${record.id}: no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min, terminating (LC-1 wedge recovery)`,
2488
+ );
2489
+ state.settledWatchdogFired = fire;
2490
+ killChildWithEscalation(state, child, "settled watchdog");
2491
+ },
2492
+ onSettleTimeout: (fire) => {
2493
+ logger.warn(
2494
+ `[session-runner] settled watchdog (settled phase) fired for ${record.id}: no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end, terminating (LC-1 wedge recovery)`,
2495
+ );
2496
+ state.settledWatchdogFired = fire;
2497
+ killChildWithEscalation(state, child, "settled watchdog");
2498
+ },
2499
+ });
2500
+ }
2501
+ }
2502
+
2503
+ /**
2504
+ * [圈复杂度门禁提取] abort 监听 + spawn watchdog 挂载(自 runSpawn 迁入,只移动
2505
+ * 代码不改行为)。返回 onAbort 供收尾 removeEventListener(注册与注销同一句柄)。
2506
+ */
2507
+ function attachAbortAndSpawnWatchdog(state: SpawnRunState, child: ChildProcess): () => void {
2508
+ const { record, opts } = state;
2509
+ // d. signal → proc.kill 监听(一次性,替代 session.abort)
2510
+ // [race-F4] 用户取消同样升级:SIGTERM 后挂住 → 30s 后 SIGKILL 兑现取消语义。
2511
+ const onAbort = (): void => {
2512
+ killPiChild(child, record.id, "abort signal");
2513
+ };
2514
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2515
+ // 前置检查:signal 在 spawn 前已 aborted 时 addEventListener 不会触发 onAbort,
2516
+ // 子进程会跑到自然结束。立即 kill 兑现取消语义。
2517
+ if (opts.signal?.aborted) onAbort();
2518
+
2519
+ // e. watchdog:子进程整体超时兜底。卡死在单 tool 内(turn_end 永不触发)时
2520
+ // limiter 失效,此 timer 保证最终 SIGTERM,防止 background 槽位/资源泄漏。
2521
+ // [M-1] timeout 基于 maxTurns 动态计算(maxTurnsToWatchdogMs):旧实现固定 30 分钟
2522
+ // 误杀长任务,现按 maxTurns 线性估算(每 turn ~5 分钟,下限 30 分钟)。
2523
+ // [预算语义对齐] maxTurns 未传/<=0 → 不挂 watchdog(不限,用户明确裁决——旧实现按
2524
+ // 10 turns 估出 50min 默认 SIGTERM 且 maxTurns:0 也关不掉,已废);
2525
+ // SPAWN_WATCHDOG_ENV 设置时按绝对时限兑底挂载(hang 泄漏防线 opt-in)。
2526
+ // [R0] unref:不阻止 Node 进程退出。安全性由 SubagentService.dispose 保证——
2527
+ // 主进程退出时(session_shutdown reason=quit)dispose 会 abort running controller
2528
+ // → 本监听器 kill 子进程。无此 unref,watchdog timer 会拖住 event loop 阻止退出。
2529
+ const watchdogMs = resolveSpawnWatchdogMs(opts.maxTurns);
2530
+ if (watchdogMs !== undefined) {
2531
+ state.watchdog = setTimeout(
2532
+ () => killPiChild(child, record.id, "spawn watchdog"),
2533
+ watchdogMs,
2534
+ );
2535
+ state.watchdog.unref();
2536
+ }
2537
+ return onAbort;
2538
+ }
2539
+
2540
+ /**
2541
+ * [圈复杂度门禁提取] get_state RPC 握手启动(自 runSpawn 迁入,fire-and-forget)。
2542
+ */
2543
+ function startGetStateHandshake(
2544
+ child: ChildProcessWithoutNullStreams,
2545
+ pump: StdoutPumpHandles,
2546
+ ): void {
2547
+ // FR-4: get_state RPC 握手——spawn 后无条件启动。
2548
+ // RPC mode(pi --mode rpc)不向 stdout 输出 header,record.sessionFile 无法靠 header
2549
+ // 推导,必须通过 get_state RPC 查询子进程回填。json mode 下 header 会先到达触发提前
2550
+ // resolve(加速路径),握手仍启动但无害——response 到达时外层已 resolve,resolver
2551
+ // 因 resolved=true 直接 return。
2552
+ //
2553
+ // 时序:握手在 stdout pump(get_stateListeners 已就绪)之后启动。get_state 命令写入
2554
+ // stdin,pi rebindSession 后读取并返回 response,经 stdout pump 匹配 resolver 触发 resolve。
2555
+ // close handler await handshakeSettled,保证无论 task 多快结束,close 时 sessionFile 已回填。
2556
+ // [#18] 握手状态变量已在 attachStdoutPump 内部(stdout handler 注册前)定义,此处直接发起握手。
2557
+ // [F2] .catch 兜底:performGetStateHandshake 的 Promise executor 同步调 tryOnce →
2558
+ // sendGetStateCommand → writeStdinLine 在 stdin 已断(EPIPE/ERR_STREAM_DESTROYED)时同步
2559
+ // throw → executor 内同步异常被 Promise 构造器转为 reject。若无人接(旧实现只有 .then),
2560
+ // reject 无人消费 → unhandledRejection(Node 15+ 默认 mode=throw)可崩父 pi 进程。
2561
+ // catch 内:logger.error 留证 + pump.abandonHandshake 记录握手失败终态——close handler
2562
+ // await 的 handshakeSettled 立即 settle,isHandshakePending() 归 false,不阻塞收尾链路
2563
+ //(sessionFile 兜底由收尾时的 existsSync 校验 + findSessionFileByHeaderId 承担)。
2564
+ void performGetStateHandshake(child, pump.registerGetStateListener).then((r) => {
2565
+ // header 加速路径下 settleHandshake 已 undefined,跳过(避免覆盖 header 结果)。
2566
+ // 超时兜底(r 为空对象)也经此分支 settle,但 record.sessionFile 不回填。
2567
+ if (pump.isHandshakePending()) pump.finishHandshake(r);
2568
+ }).catch((err: unknown) => {
2569
+ const m = toErrorMessage(err);
2570
+ logger.error(`[session-runner] get_state handshake failed: ${m}`);
2571
+ pump.abandonHandshake();
2572
+ });
2573
+ }
2574
+
2575
+ /**
2576
+ * [圈复杂度门禁提取] sessionFile 兜底反查(LC-4,自 runSpawn 收尾迁入)。
2577
+ */
2578
+ function backfillSessionFileByLookup(state: SpawnRunState, sessionDir: string): void {
2579
+ const { record } = state;
2580
+ // [持久化 A] sessionFile 兜底校验。
2581
+ // identity custom entry 已改由子进程 session_start hook 写(M4 / V2 决策 5),
2582
+ // 父进程不再 fs 补写——fs 补写的 entry 缺 id/parentId 污染 pi _buildIndex。
2583
+ // 此处仅保留 sessionFile 路径兜底(deriveSessionFilePath/握手路径可能不准)。
2584
+ //
2585
+ // [T1/LC-4] findSessionFileByHeaderId 兜底查找移出 `if (record.sessionFile)` 守卫:
2586
+ // 原守卫条件恰等于它要兜底的缺失本身——RC-1 握手失败留下的「sessionId 有、
2587
+ // sessionFile 无」形态(两字段独立采集)永远不可达本兜底,下游 finalize marker /
2588
+ // alive marker / identity 写入全部失去依据(PS-9 放大)。现在两种形态都反查:
2589
+ // sessionFile 有但路径不存在(推导错 / pi 命名变化);sessionFile 无而 sessionId 有
2590
+ // (header 或握手部分成功,含 agent_end 惰性回补补入的 handshakeResult.sessionId)。
2591
+ const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
2592
+ const needsLookup = record.sessionFile
2593
+ ? !fs.existsSync(record.sessionFile)
2594
+ : lookupId !== undefined;
2595
+ if (lookupId && needsLookup) {
2596
+ const actual = findSessionFileByHeaderId(sessionDir, lookupId);
2597
+ if (actual && actual !== record.sessionFile) record.sessionFile = actual;
2598
+ }
2599
+ }
2600
+
2601
+ /**
2602
+ * [圈复杂度门禁提取] keep-alive 上界处置的两步时序后半(T2-② 后代级联补杀,
2603
+ * 自 runSpawn 收尾迁入):close 确认死亡 + sessionFile 冻结为最终快照后 sweep。
2604
+ */
2605
+ function sweepDescendantsOnChildClose(state: SpawnRunState, sessionDir: string): void {
2606
+ const { record } = state;
2607
+ // [T2-② / P-T2b 主路径] keep-alive 上界处置层主的两步时序后半:此刻 close 已发生
2608
+ //(waitForChildExit 已 resolve = 层主确认死亡),且 sessionFile 已完成 LC-4 反查
2609
+ // = 冻结为最终快照(pending entries 最完整,避开「kill 前采集」的垂死窗口漏项)。
2610
+ // 从层主 sessionFile 差集采集活跃后代清单,对清单内每个后代迭代展开至叶(递归读
2611
+ // 各后代的 pending 差集),kill 前做存活 + cmdline(pi/--mode rpc)校验防 pid 复用
2612
+ // 误杀,逐个 escalation kill(SIGTERM→SIGKILL)。同步执行:后代树规模有限 + 每
2613
+ // 步有界(ps 探测 3s 超时),不阻塞 runSpawn 收尾的可感时长。
2614
+ if (state.sweepDescendantsOnClose) {
2615
+ try {
2616
+ const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
2617
+ if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
2618
+ logger.warn(
2619
+ `[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`,
2620
+ );
2621
+ }
2622
+ } catch (err) {
2623
+ // sweep 整体失败不掩盖层主自身的收尾结果(best-effort 可见)
2624
+ bestEffort(err, "descendant sweep (keep-alive watchdog)", "error");
2625
+ }
2626
+ }
2627
+ }
2628
+
2629
+ /**
2630
+ * [圈复杂度门禁提取] 成功/失败判定(三来源:exitCode + record.lastError + abort
2631
+ * 原因;自 runSpawn 迁入,四分支行为逐字节保持)。
2632
+ */
2633
+ function resolveRunOutcome(
2634
+ state: SpawnRunState,
2635
+ exitCode: number,
2636
+ stderrBuffer: string,
2637
+ ): { success: boolean; error: string | undefined } {
2638
+ const { record, opts } = state;
2639
+ let success: boolean;
2640
+ let error: string | undefined;
2641
+ if (state.settledWatchdogFired) {
2642
+ // [T2-③ / D9] settled watchdog 到期回收 ≠ 正常完成:被信号终止视为正常完成的
2643
+ // 既有语义(maxTurns 达限 kill)不适用于本形态——runSpawn 以错误返回(设计
2644
+ // §6.2),错误消息含恢复指引(S-B 验收判据)。closedReason 的 "watchdog" 映射
2645
+ // 由 finalize 侧按 error 标记承接(ClosedReason 枚举封闭,不在此擅自扩枚举)。
2646
+ // 文案按触发段分叉窗长语义(D9 失败路径总表:中段标注 mid-round no-progress)。
2647
+ const fire = state.settledWatchdogFired;
2648
+ const windowDesc =
2649
+ fire.phase === "mid-round"
2650
+ ? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min after prompt (mid-round no-progress)`
2651
+ : `no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end (settled phase)`;
2652
+ success = false;
2653
+ error =
2654
+ `subagent did not reach agent_settled (${windowDesc}; settled watchdog); the process was terminated to bound the wait. ` +
2655
+ `Recovery: check state with subagents action:'list', then re-send your message to continue.`;
2656
+ } else if (record.lastError) {
2657
+ // LLM/provider error 或 abort error 已收口进 record.lastError
2658
+ success = false;
2659
+ error = record.lastError;
2660
+ } else if (exitCode !== 0 && exitCode < SIGNAL_EXIT_CODE_THRESHOLD) {
2661
+ // 非信号退出的非零 exit code = 子进程自身报错
2662
+ success = false;
2663
+ error = stderrBuffer.trim() || `pi subprocess exited with code ${exitCode}`;
2664
+ } else if (opts.signal?.aborted) {
2665
+ // 用户/调用方 signal 取消(非 maxTurns)——不算成功,但也不算 error
2666
+ success = false;
2667
+ error = undefined;
2668
+ } else {
2669
+ // exitCode === 0 或被信号终止(maxTurns 达限 kill)——均视为正常完成
2670
+ success = true;
2671
+ error = record.lastError;
2672
+ }
2673
+ return { success, error };
2674
+ }
2675
+
2277
2676
  /**
2278
2677
  * spawn pi 子进程执行 session。
2279
2678
  *
@@ -2307,13 +2706,12 @@ export async function runSpawn(
2307
2706
  ctx,
2308
2707
  proc: undefined,
2309
2708
  watchdog: undefined,
2310
- escalationTimer: undefined,
2311
2709
  sessionHeader: undefined,
2312
2710
  handshakeResult: undefined,
2313
2711
  resolveRun: undefined,
2314
2712
  keepAliveNoProgressTimer: undefined,
2315
2713
  sweepDescendantsOnClose: false,
2316
- settledWatchdogFired: false,
2714
+ settledWatchdogFired: undefined,
2317
2715
  };
2318
2716
 
2319
2717
  // a/b. 事件累积器(pendingTools 寄存器 + turnLimiter + handleSdkEvent/agentEvent 闭包)
@@ -2370,124 +2768,16 @@ export async function runSpawn(
2370
2768
  env: childEnv,
2371
2769
  });
2372
2770
  state.proc = child;
2373
- // [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
2374
- // 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
2375
- // RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
2376
- // SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
2377
- // header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
2378
- if (opts.worktree && child.pid) {
2379
- // [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
2380
- void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
2381
- }
2382
- // [C1] track 子进程供 dispose 兜底 kill(sync + background 均注册——sync 无 controller,
2383
- // abortRunningControllers 跳过它,靠本 Map 兜底)。close/error 后按句守卫移除(已退出无需再 kill)。
2384
- spawnedChildren.set(record.id, child);
2385
- // [V2 决策 3] spawn 后 child.pid 同步立即可得,记录到 record 内存(lifecycle-manager
2386
- // 孤儿扫描用,Step 5 接入持久化)。resume spawn 时此处同样覆盖更新(pid 可能已变)。
2387
- if (child.pid !== undefined) record.pid = child.pid;
2388
-
2389
- // [worktree-reaper-fix] 同步补全注册表 pid:spawn 返回后 child.pid 立即可得(Node.js
2390
- // 同步属性),无需等任何 stdout 事件。原补全点挂在 header 分支(下方 stdout handler 内),
2391
- // 而 RPC mode(buildSpawnArgs 固定 --mode rpc)不输出 header 行——pid 恒为 0,超
2392
- // SPAWN_GRACE_MS 后被 reaper 当孤儿误删活 worktree(2026-08-11 cw 递归编排整树失活事故)。
2393
- // header 分支调用保留:json mode 回切时仍能补全,updatePid 同 branch 覆盖写幂等,无副作用。
2394
- // [S1] 防御:必须放在 spawnedChildren.add 之后(onWorktreePid 抛错时子进程已被跟踪,
2395
- // dispose 兜底 kill 不会泄漏),且包 try/catch(补全失败不阻断 spawn 主流程——
2396
- // 注册表写失败最坏后果是条目停留 pid=0,由 reaper 宽限回收兜底)。
2397
- if (opts.worktree && child.pid) {
2398
- try {
2399
- // [D5a] void:回调可能返回 Promise(锁内 RMW),契约保证不 reject,fire-and-forget。
2400
- void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
2401
- } catch (err) {
2402
- logger.warn("[worktree] worktree pid registration failed (defensive)", {
2403
- branch: opts.worktree.branch,
2404
- pid: child.pid,
2405
- err: err instanceof Error ? err.message : String(err),
2406
- });
2407
- }
2408
- }
2771
+ // [圈复杂度门禁提取] spawn 后同步装配(worktree pid 补全 ×2 / 记账 / record.pid /
2772
+ // stdio 编码 / stdin error listener / prompt 命令)整段迁入 setupFreshChild。
2773
+ setupFreshChild(state, child, task);
2409
2774
 
2410
- // stdout/stderr utf8 编码:stream 自动按字符边界切分,避免多字节
2411
- // UTF-8(CJK/emoji)跨 chunk 时 toString() 产生 U+FFFD 替换符导致 JSON.parse 失败。
2412
- // [m2] 先 setEncoding 再注册 signal listener/watchdog:若 setEncoding 抛错,try/finally
2413
- //(下方)只清理 tempPromptFile,watchdog/signal listener 尚未注册则无需清理——避免泄漏。
2414
- child.stdout.setEncoding("utf8");
2415
- child.stderr.setEncoding("utf8");
2416
-
2417
- // [v4 A-1] 异步 stdin 'error' listener。writeStdinLine 的 try/catch 只覆盖同步 write 抛错;
2418
- // 子进程退出后内核回写 EPIPE 会以异步 stream 'error' event 到达,若无 listener 会让 Node
2419
- // 抛 unhandled 'error' 崩主进程(P1)。必须在首次 stdin 写入(下方 sendPromptCommand)前注册。
2420
- // handler 行为:①移出 spawnedChildren 标记 dead(与 child.on('error') 同模式);
2421
- // ②recordEpipeFailure 合并同步/异步计数;③logger.warn 记录一次。
2422
- // [v4 A-1 裁决] handler 内**不 throw**——stream 'error' listener 内 throw 会经 Node 内部
2423
- // emit() 传播为 uncaughtException 崩主进程,违背 A-1 防崩核心目标。达阈值的 throw 留给
2424
- // 同步路径(deliverMessage catch 合并计数达 EPIPE_FAILURE_THRESHOLD 时同步 throw,不崩)。
2425
- // async handler 只移句柄 + 计数 + warn;进程已 dead(移句柄),下次 deliverMessage 检测
2426
- // dead 走冷路径,冷路径 write EPIPE 同步计数达阈值同步 throw——防死循环且不崩。
2427
- child.stdin.on("error", (err: Error) => {
2428
- // [M4] 按值守卫:resume spawn 已覆盖注册时不误删新 child(见 removeChildRegistration)
2429
- removeChildRegistration(record.id, child);
2430
- const count = recordEpipeFailure(record.id);
2431
- logger.warn(`[subagents] async stdin error for ${record.id}`, {
2432
- detail: err.message,
2433
- epipeCount: count,
2434
- threshold: EPIPE_FAILURE_THRESHOLD,
2435
- hint:
2436
- count >= EPIPE_FAILURE_THRESHOLD
2437
- ? "sync path will throw on next write EPIPE"
2438
- : undefined,
2439
- });
2440
- });
2441
-
2442
- // 喂 prompt 命令驱动子进程开始处理 task。pi runRpcMode 只消费 stdin RpcCommand,
2443
- // 不读 positional arg;必须在 spawn 后主动写,否则子进程阻塞、totalTokens 恒 0。
2444
- // 时机安全:pipe 内核缓冲不丢;pi 在 rebindSession 后才挂 stdin reader。
2445
- sendPromptCommand(child, task);
2446
-
2447
- // [T2-③ / LC-1] chatMode 首轮 settled 等待固定硬上限(双挂载原语之首轮调用点;
2448
- // 热路径调用点在 subagent-service deliverMessage,u-t2b 接线)。prompt 发出后挂、
2449
- // settled 到达(handleSdkEvent)/ close(waitForChildExit)/ resolveRun 任一发生
2450
- // 即清——settled 永不到达(事件行丢失 / 子进程 wedged)时本 timer 是唯一独立
2451
- // 回收通道。到期处置:kill 层主 + settledWatchdogFired 标记(收尾据此转 failed
2452
- // + 恢复指引,见下方 success 判定),与被信号终止视为正常完成的既有语义区分。
2453
- if (record.chatMode) {
2454
- armSettledWatchdog(record.id, () => {
2455
- logger.warn(
2456
- `[session-runner] settled watchdog fired for ${record.id}: no agent_settled within ${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min of first-round prompt, terminating (LC-1 wedge recovery)`,
2457
- );
2458
- state.settledWatchdogFired = true;
2459
- killChildWithEscalation(state, child, "settled watchdog");
2460
- });
2461
- }
2775
+ // [圈复杂度门禁提取] chatMode 首轮 settled 守护挂载迁入 armChatModeSettledWatchdog。
2776
+ armChatModeSettledWatchdog(state, child);
2462
2777
 
2463
- // d. signal proc.kill 监听(一次性,替代 session.abort)
2464
- // [race-F4] 用户取消同样升级:SIGTERM 后挂住 → 30s 后 SIGKILL 兑现取消语义。
2465
- const onAbort = (): void => {
2466
- killChildWithEscalation(state, child, "abort signal");
2467
- };
2468
- opts.signal?.addEventListener("abort", onAbort, { once: true });
2469
- // 前置检查:signal 在 spawn 前已 aborted 时 addEventListener 不会触发 onAbort,
2470
- // 子进程会跑到自然结束。立即 kill 兑现取消语义。
2471
- if (opts.signal?.aborted) onAbort();
2472
-
2473
- // e. watchdog:子进程整体超时兜底。卡死在单 tool 内(turn_end 永不触发)时
2474
- // limiter 失效,此 timer 保证最终 SIGTERM,防止 background 槽位/资源泄漏。
2475
- // [M-1] timeout 基于 maxTurns 动态计算(maxTurnsToWatchdogMs):旧实现固定 30 分钟
2476
- // 误杀长任务,现按 maxTurns 线性估算(每 turn ~5 分钟,下限 30 分钟)。
2477
- // [预算语义对齐] maxTurns 未传/<=0 → 不挂 watchdog(不限,用户明确裁决——旧实现按
2478
- // 10 turns 估出 50min 默认 SIGTERM 且 maxTurns:0 也关不掉,已废);
2479
- // SPAWN_WATCHDOG_ENV 设置时按绝对时限兑底挂载(hang 泄漏防线 opt-in)。
2480
- // [R0] unref:不阻止 Node 进程退出。安全性由 SubagentService.dispose 保证——
2481
- // 主进程退出时(session_shutdown reason=quit)dispose 会 abort running controller
2482
- // → 本监听器 kill 子进程。无此 unref,watchdog timer 会拖住 event loop 阻止退出。
2483
- const watchdogMs = resolveSpawnWatchdogMs(opts.maxTurns);
2484
- if (watchdogMs !== undefined) {
2485
- state.watchdog = setTimeout(
2486
- () => killChildWithEscalation(state, child, "spawn watchdog"),
2487
- watchdogMs,
2488
- );
2489
- state.watchdog.unref();
2490
- }
2778
+ // [圈复杂度门禁提取] abort 监听 + spawn watchdog 挂载迁入 attachAbortAndSpawnWatchdog,
2779
+ // 返回 onAbort 供下方收尾 removeEventListener(注册与注销同一句柄)。
2780
+ const onAbort = attachAbortAndSpawnWatchdog(state, child);
2491
2781
 
2492
2782
  // [recursive-orchestration] agent_end 有活跃后代时的等待超时(不 kill 分支的兑底)。
2493
2783
  // 层主 subagent 空闲等待后代完成(steer 唤醒)期间不产生 turn,原 watchdog 已清;
@@ -2497,32 +2787,9 @@ export async function runSpawn(
2497
2787
  // stdout pump:逐行解析 → handleSdkEvent / enqueueUiRequest + get_state 握手状态机
2498
2788
  const pump = attachStdoutPump(child, state, sessionDir, handleSdkEvent);
2499
2789
 
2500
- // FR-4: get_state RPC 握手——spawn 后无条件启动。
2501
- // RPC mode(pi --mode rpc)不向 stdout 输出 header,record.sessionFile 无法靠 header
2502
- // 推导,必须通过 get_state RPC 查询子进程回填。json mode 下 header 会先到达触发提前
2503
- // resolve(加速路径),握手仍启动但无害——response 到达时外层已 resolve,resolver
2504
- // 因 resolved=true 直接 return。
2505
- //
2506
- // 时序:握手在 stdout pump(get_stateListeners 已就绪)之后启动。get_state 命令写入
2507
- // stdin,pi rebindSession 后读取并返回 response,经 stdout pump 匹配 resolver 触发 resolve。
2508
- // close handler await handshakeSettled,保证无论 task 多快结束,close 时 sessionFile 已回填。
2509
- // [#18] 握手状态变量已在 attachStdoutPump 内部(stdout handler 注册前)定义,此处直接发起握手。
2510
- // [F2] .catch 兜底:performGetStateHandshake 的 Promise executor 同步调 tryOnce →
2511
- // sendGetStateCommand → writeStdinLine 在 stdin 已断(EPIPE/ERR_STREAM_DESTROYED)时同步
2512
- // throw → executor 内同步异常被 Promise 构造器转为 reject。若无人接(旧实现只有 .then),
2513
- // reject 无人消费 → unhandledRejection(Node 15+ 默认 mode=throw)可崩父 pi 进程。
2514
- // catch 内:logger.error 留证 + pump.abandonHandshake 记录握手失败终态——close handler
2515
- // await 的 handshakeSettled 立即 settle,isHandshakePending() 归 false,不阻塞收尾链路
2516
- //(sessionFile 兜底由收尾时的 existsSync 校验 + findSessionFileByHeaderId 承担)。
2517
- void performGetStateHandshake(child, pump.registerGetStateListener).then((r) => {
2518
- // header 加速路径下 settleHandshake 已 undefined,跳过(避免覆盖 header 结果)。
2519
- // 超时兜底(r 为空对象)也经此分支 settle,但 record.sessionFile 不回填。
2520
- if (pump.isHandshakePending()) pump.finishHandshake(r);
2521
- }).catch((err: unknown) => {
2522
- const m = err instanceof Error ? err.message : String(err);
2523
- logger.error(`[session-runner] get_state handshake failed: ${m}`);
2524
- pump.abandonHandshake();
2525
- });
2790
+ // [圈复杂度门禁提取] get_state 握手启动(fire-and-forget + F2 catch 兜底)迁入
2791
+ // startGetStateHandshake。
2792
+ startGetStateHandshake(child, pump);
2526
2793
 
2527
2794
  child.stderr.on("data", (data: string) => {
2528
2795
  // 截断防 OOM:失控子进程持续打 stderr 会耗尽父进程内存。保留尾部便于诊断。
@@ -2534,85 +2801,23 @@ export async function runSpawn(
2534
2801
 
2535
2802
  opts.signal?.removeEventListener("abort", onAbort);
2536
2803
  clearTimeout(state.watchdog);
2537
- // [race-F4] 兑底清升级 timer(exit 事件自动 clear 的双保险:close 先于升级触发的
2538
- // 竞态窗口内不误杀下一个占用同 state 的子进程)
2539
- clearTimeout(state.escalationTimer);
2804
+ // [D3-①] SIGKILL 升级 timer 的清理由公共 kill-chain 内部承担(exit 事件 settle
2805
+ // 等待 promise → raceTimeout clearTimeout;且链持有 child 闭包,不误伤后续同
2806
+ // state 子进程)——旧 escalationTimer 兜底句柄随旧私有杀链一并删除。
2540
2807
  // [T2-①③] 收尾兜底清新增 timer(正常清除点已覆盖;防收尾路径遗漏泄漏)
2541
2808
  disarmKeepAliveNoProgressTimer(state);
2542
2809
  disarmSettledWatchdog(record.id);
2543
2810
 
2544
- // [持久化 A] sessionFile 兜底校验。
2545
- // identity custom entry 已改由子进程 session_start hook 写(M4 / V2 决策 5),
2546
- // 父进程不再 fs 补写——fs 补写的 entry 缺 id/parentId 污染 pi _buildIndex。
2547
- // 此处仅保留 sessionFile 路径兜底(deriveSessionFilePath/握手路径可能不准)。
2548
- //
2549
- // [T1/LC-4] findSessionFileByHeaderId 兜底查找移出 `if (record.sessionFile)` 守卫:
2550
- // 原守卫条件恰等于它要兜底的缺失本身——RC-1 握手失败留下的「sessionId 有、
2551
- // sessionFile 无」形态(两字段独立采集)永远不可达本兜底,下游 finalize marker /
2552
- // alive marker / identity 写入全部失去依据(PS-9 放大)。现在两种形态都反查:
2553
- // sessionFile 有但路径不存在(推导错 / pi 命名变化);sessionFile 无而 sessionId 有
2554
- // (header 或握手部分成功,含 agent_end 惰性回补补入的 handshakeResult.sessionId)。
2555
- {
2556
- const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
2557
- const needsLookup = record.sessionFile
2558
- ? !fs.existsSync(record.sessionFile)
2559
- : lookupId !== undefined;
2560
- if (lookupId && needsLookup) {
2561
- const actual = findSessionFileByHeaderId(sessionDir, lookupId);
2562
- if (actual && actual !== record.sessionFile) record.sessionFile = actual;
2563
- }
2564
- }
2811
+ // [圈复杂度门禁提取] sessionFile 兜底反查(LC-4)迁入 backfillSessionFileByLookup。
2812
+ backfillSessionFileByLookup(state, sessionDir);
2565
2813
 
2566
- // [T2-② / P-T2b 主路径] keep-alive 上界处置层主的两步时序后半:此刻 close 已发生
2567
- //(waitForChildExit 已 resolve = 层主确认死亡),且 sessionFile 已完成 LC-4 反查
2568
- // = 冻结为最终快照(pending entries 最完整,避开「kill 前采集」的垂死窗口漏项)。
2569
- // 从层主 sessionFile 差集采集活跃后代清单,对清单内每个后代迭代展开至叶(递归读
2570
- // 各后代的 pending 差集),kill 前做存活 + cmdline(pi/--mode rpc)校验防 pid 复用
2571
- // 误杀,逐个 escalation kill(SIGTERM→SIGKILL)。同步执行:后代树规模有限 + 每
2572
- // 步有界(ps 探测 3s 超时),不阻塞 runSpawn 收尾的可感时长。
2573
- if (state.sweepDescendantsOnClose) {
2574
- try {
2575
- const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
2576
- if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
2577
- logger.warn(
2578
- `[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`,
2579
- );
2580
- }
2581
- } catch (err) {
2582
- // sweep 整体失败不掩盖层主自身的收尾结果(best-effort 可见)
2583
- bestEffort(err, "descendant sweep (keep-alive watchdog)", "error");
2584
- }
2585
- }
2814
+ // [圈复杂度门禁提取] 后代级联补杀 sweep(T2-② 两步时序后半)迁入
2815
+ // sweepDescendantsOnChildClose。
2816
+ sweepDescendantsOnChildClose(state, sessionDir);
2586
2817
 
2587
2818
  // 判定成功/失败(三来源:exitCode + record.lastError + abort 原因)
2588
- let success: boolean;
2589
- let error: string | undefined;
2590
- if (state.settledWatchdogFired) {
2591
- // [T2-③ / LC-1] settled 硬上限到期回收 ≠ 正常完成:被信号终止视为正常完成的
2592
- // 既有语义(maxTurns 达限 kill)不适用于本形态——runSpawn 以错误返回(设计
2593
- // §6.2),错误消息含恢复指引(S-B 验收判据)。closedReason 的 "watchdog" 映射
2594
- // 由 finalize 侧按 error 标记承接(ClosedReason 枚举封闭,不在此擅自扩枚举)。
2595
- success = false;
2596
- error =
2597
- `subagent did not reach agent_settled within ${SETTLED_WATCHDOG_TIMEOUT_MS / MS_PER_SECOND / SECONDS_PER_MINUTE} min (settled watchdog); the process was terminated to bound the wait. ` +
2598
- `Recovery: check state with subagents action:'list', then re-send your message to continue.`;
2599
- } else if (record.lastError) {
2600
- // LLM/provider error 或 abort error 已收口进 record.lastError
2601
- success = false;
2602
- error = record.lastError;
2603
- } else if (exitCode !== 0 && exitCode < SIGNAL_EXIT_CODE_THRESHOLD) {
2604
- // 非信号退出的非零 exit code = 子进程自身报错
2605
- success = false;
2606
- error = stderrBuffer.trim() || `pi subprocess exited with code ${exitCode}`;
2607
- } else if (opts.signal?.aborted) {
2608
- // 用户/调用方 signal 取消(非 maxTurns)——不算成功,但也不算 error
2609
- success = false;
2610
- error = undefined;
2611
- } else {
2612
- // exitCode === 0 或被信号终止(maxTurns 达限 kill)——均视为正常完成
2613
- success = true;
2614
- error = record.lastError;
2615
- }
2819
+ // [圈复杂度门禁提取] 四分支判定迁入 resolveRunOutcome(行为逐字节保持)。
2820
+ const { success, error } = resolveRunOutcome(state, exitCode, stderrBuffer);
2616
2821
 
2617
2822
  // g. collectResult(完全复用——全部从 record 读)
2618
2823
  // [F-1] schemaExpected:schema/schemaEnv 任一存在即要求结构化产出(耦合形态