@zhushanwen/subagent-core 0.2.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 (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,1018 @@
1
+ /**
2
+ * Workflow Extension — error-recovery
3
+ *
4
+ * Worker 失败处理 free functions(D-12)。
5
+ *
6
+ * 4 个导出函数(domain-models.md §失败处理矩阵):
7
+ * - handleWorkerMessage(run, raw, deps, handlers) — 路由 agent_call/return/error
8
+ * - handleWorkerError(run, err, deps, handlers) — worker uncaught error
9
+ * - handleWorkerExit(run, code, handle, deps, handlers) — worker exit
10
+ * - handleScriptError(run, msg, deps, handlers) — type:"error" from worker
11
+ *
12
+ * 重试矩阵(domain-models.md §失败处理矩阵):
13
+ * - worker error/exit(非零)→ 3 次重试 + 指数退避 1s/2s/4s;超限 failed
14
+ * - script error → 3 次重试 + 指数退避;超限 failed
15
+ * - 重试前 rebuildRuntime(G3-001:整个 RunRuntime 重建:worker+controller)
16
+ *
17
+ * 关键不变式:
18
+ * - 重试前必须 rebuildRuntime(worker+controller 整体重建,避免孤儿资源)。
19
+ * - 重试计数载体是 run.meta.workerErrorCount/scriptErrorCount(跨 runtime 存活,
20
+ * retry replaceRuntime 后计数不丢)。
21
+ * - handleWorkerExit 检查 handle.isCurrent(G-025:stale exit 事件丢弃)。
22
+ *
23
+ * 层归属:Engine。依赖 ports + WorkflowRun + executeAgentCall。
24
+ * (旧并发门闩 gate 抽象已删——no-op,实际并发由 SubagentService ConcurrencyPool 管理。)
25
+ *
26
+ * 参考:domain-models.md §失败处理矩阵。
27
+ */
28
+
29
+ import { getLogger } from "../core/logger.ts";
30
+
31
+ import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
32
+ import { createRecord, updateFromEvent } from "../execution/execution-record.ts";
33
+ import { SubagentStream } from "../execution/stream-sink.ts";
34
+ import type { AgentEvent } from "../shared/agent-event.ts";
35
+ import { resolveAgentOpts } from "./agent-opts-resolver.ts";
36
+ import { executeAgentCall } from "./execute-agent-call.ts";
37
+ import { AgentCall } from "./models/agent-call.ts";
38
+ import type { LifecycleDeps, WorkerHandlers } from "./models/ports.ts";
39
+ import { RunRuntime } from "./models/run-runtime.ts";
40
+ import type { WorkerLogEntry } from "./models/types.ts";
41
+ import type { AgentCallOpts, AgentResult, ExecutionTraceNode } from "./models/types.ts";
42
+ import type { WorkflowRun } from "./models/workflow-run.ts";
43
+ import type { WorkerHandle } from "./worker-handle.ts";
44
+
45
+ const logger = getLogger("subagents");
46
+
47
+ // ── 常量 ─────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * 单类错误最大重试次数(domain-models.md §失败处理矩阵)。
51
+ *
52
+ * 注意:workerErrorCount 和 scriptErrorCount 是两个独立计数器,各自上限 MAX_WORKER_RETRIES。
53
+ * 最坏情况(先连续 worker error 3 次 + 再连续 script error 3 次)= 6 次 rebuild。
54
+ * 这是有意设计——两类错误的根因不同(worker 崩溃 vs 脚本逻辑),合并计数会导致
55
+ * 不同根因的失败被过早判 failed。scheduleRebuild 的 retryIndex 取 max(两计数)。
56
+ */
57
+ const MAX_WORKER_RETRIES = 3;
58
+
59
+ /** 指数退避基数(ms)。 */
60
+ const RETRY_BACKOFF_BASE_MS = 1000;
61
+ const EXPONENTIAL_BACKOFF_BASE = 2;
62
+
63
+ /** errorLogs 最大保留条数(防止超长 session 中日志无界增长)。 */
64
+ const MAX_ERROR_LOGS = 500;
65
+
66
+ /** malformed agent-call 日志中 opts JSON 的预览截断长度(字符)。 */
67
+ const MALFORMED_MSG_LOG_PREVIEW_CHARS = 200;
68
+
69
+ /**
70
+ * [F1] worker 交付前退出(无终态消息)的归因文案。
71
+ *
72
+ * 最常见根因:execute() 返回值含 function/Symbol/循环引用等不可克隆成员 → worker 侧
73
+ * _safePost 吞掉 DataCloneError → return 消息从未发出 → worker exit(0)。旧实现
74
+ * handleWorkerExit 对 code===0 no-op → run 永久 running、runAndWait 悬挂。
75
+ */
76
+ const WORKER_EXITED_WITHOUT_RESULT_MSG =
77
+ "worker exited before delivering a result (return value may not be structured-cloneable)";
78
+
79
+ // ── Worker 消息类型(与 infra/worker-script-builder.ts WorkerInMsg 对齐) ──
80
+
81
+ interface AgentCallMsg {
82
+ type: "agent-call";
83
+ callId: number;
84
+ opts: {
85
+ prompt: string;
86
+ schema?: unknown;
87
+ model?: string;
88
+ scene?: string;
89
+ description?: string;
90
+ agent?: string;
91
+ skill?: string;
92
+ timeoutMs?: number;
93
+ cwd?: string; // ADR-029 决策 1:per-call cwd(worktree 隔离)
94
+ };
95
+ phase?: string;
96
+ }
97
+
98
+ interface ReturnMsg {
99
+ type: "return";
100
+ result: unknown;
101
+ workerLogs?: WorkerLogEntry[];
102
+ }
103
+
104
+ interface ErrorMsg {
105
+ type: "error";
106
+ error: string;
107
+ workerLogs?: WorkerLogEntry[];
108
+ }
109
+
110
+ interface WorkflowCallMsg {
111
+ type: "workflow-call";
112
+ callId: number;
113
+ name: string;
114
+ args: Record<string, unknown>;
115
+ }
116
+
117
+ type WorkerMsg = AgentCallMsg | WorkflowCallMsg | ReturnMsg | ErrorMsg;
118
+
119
+ // ── 内部 helper ──────────────────────────────────────────────
120
+
121
+ function isTerminal(run: WorkflowRun): boolean {
122
+ return run.state.status === "done";
123
+ }
124
+
125
+ /**
126
+ * 孤儿 call 判定:dispatch 时捕获的 call 实例是否已不是 calls Map 中该 callId
127
+ * 的当前条目。
128
+ *
129
+ * 为何需要实例级比对(而非只查 run 状态):rebuildRuntime 不改 status(全程
130
+ * running),既有的终态 stale 守卫拦不住旧 runtime 代际的迟到 completion。只有
131
+ * discardInFlightCalls(delete 条目)与新一代 dispatch(set 新实例)会改变
132
+ * 「callId → 实例」映射,故实例不等 ⟺ 本 completion 属于被丢弃/被替换的旧代际
133
+ * (S7-second 竞态:旧失败结果经 postAgentResult 投给新 worker 的同 callId
134
+ * pending,劫持重跑调用为假失败/空串假成功)。
135
+ *
136
+ * 运行期 calls Map 写点仅 discard 的 delete 与 dispatchAgentCall 的 set 两族
137
+ * (jsonl-run-store 的 set 在离线重水合路径,无在飞 promise),正常(非孤儿)
138
+ * 路径下实例恒等,无误判。
139
+ */
140
+ function isOrphanedCall(run: WorkflowRun, callId: number, call: AgentCall): boolean {
141
+ return run.state.calls.get(callId) !== call;
142
+ }
143
+
144
+ /** 计算第 n 次重试前的退避时间(ms):1s, 2s, 4s 指数。 */
145
+ function backoffDelay(retryIndex: number): number {
146
+ return RETRY_BACKOFF_BASE_MS * Math.pow(EXPONENTIAL_BACKOFF_BASE, retryIndex - 1);
147
+ }
148
+
149
+ /**
150
+ * [SW-DATA-3] store.save 尽力持久化:save 抛错(如 ENOSPC 磁盘满)不阻断状态机推进。
151
+ *
152
+ * save 失败若向上抛,handle* 的调用方(worker-host 绑定处 `void handlers.onXxx(...)`)
153
+ * 无人接 → unhandledRejection + 后续 pending:unregister / onRunDone 不执行 → pending
154
+ * 通知幽灵注销(列表残留永不清理的 running 条目)。catch 后记 error 日志,调用方继续
155
+ * emit/onRunDone(内存态已终态;落盘失败仅丢本次持久化快照,kill-9 恢复时残留 running
156
+ * 由 session_start 兜底转 failed)。
157
+ */
158
+ async function saveRunBestEffort(
159
+ run: WorkflowRun,
160
+ deps: LifecycleDeps,
161
+ context: string,
162
+ ): Promise<void> {
163
+ try {
164
+ await deps.store.save(run);
165
+ } catch (err) {
166
+ const m = err instanceof Error ? err.message : String(err);
167
+ logger.error(
168
+ `[workflow] store.save failed (${context}, runId=${run.runId}): ${m}. ` +
169
+ "Continuing state-machine finalization (in-memory state already terminal).",
170
+ );
171
+ }
172
+ }
173
+
174
+ function delay(ms: number): Promise<void> {
175
+ return new Promise((resolve) => {
176
+ const timer = setTimeout(resolve, ms);
177
+ timer.unref();
178
+ });
179
+ }
180
+
181
+ // ── rebuildRuntime(G3-001 整重建) ─────────────────────────
182
+
183
+ /**
184
+ * 移除 run 中未真正完成的在飞 call(status !== "done")及其 trace 节点。
185
+ *
186
+ * 仅 rebuildRuntime 调用——清理被旧 runtime abort 的在飞 call,避免重跑时
187
+ * cached replay 把 abort 产生的 failed 结果当作已完成结果回放(原 MUST_FIX
188
+ * round-4 #1,自 pause 路径移入崩溃重建路径)。genuinely-done 的 call(成功或
189
+ * 失败均 "done")保留,重跑时按原语义 replay(不重复耗 token)。
190
+ *
191
+ * 返回被丢弃的 callId 数组(升序——Map 迭代按插入序,排序保证返回值与
192
+ * rebuildRuntime 的 L3 日志 payload 形态稳定),供调用方记日志。
193
+ */
194
+ function discardInFlightCalls(run: WorkflowRun): number[] {
195
+ const inFlight: number[] = [];
196
+ for (const [callId, call] of run.state.calls) {
197
+ if (call.status !== "done") inFlight.push(callId);
198
+ }
199
+ for (const callId of inFlight) {
200
+ run.state.calls.delete(callId);
201
+ run.state.trace.removeByStepIndex(callId);
202
+ }
203
+ return inFlight.sort((a, b) => a - b);
204
+ }
205
+
206
+ /**
207
+ * 计算 run 的剩余时间预算(ms)[race-F3]。
208
+ *
209
+ * 未配置预算(budgetTimeMs 未设或 <=0,默认不限)返回 undefined;已配置时返回
210
+ * max(0, budgetTimeMs - 已耗墙钟),已耗墙钟从 run.meta.startedAt(ISO)推算——
211
+ * 含退避等待在内的全部 wall clock,重试不重置预算。startedAt 解析失败(损坏快照)
212
+ * 防御性按 0 已耗处理(给满额预算,不因元数据损坏提前杀 run)。
213
+ *
214
+ * 背景:rebuildRuntime 重排计时器原样用满额 budgetTimeMs——每吃一次 worker/script
215
+ * 错误重试就重置一次预算,最坏 6 次重试放大 ~6× 墙钟,时间预算对重试路径失效。
216
+ */
217
+ function remainingTimeBudgetMs(run: WorkflowRun): number | undefined {
218
+ const budget = run.spec.budgetTimeMs;
219
+ if (!budget || budget <= 0) return undefined;
220
+ const startedMs = Date.parse(run.meta.startedAt);
221
+ const elapsed = Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0;
222
+ return Math.max(0, budget - elapsed);
223
+ }
224
+
225
+ /**
226
+ * 重试前发现时间预算已耗尽的收尾:不 rebuild,直接 done,time_limited 终态。
227
+ *
228
+ * 副作用与 handleWorkerError 超限路径对齐:transition + 持久化 + 注销
229
+ * pending-notification + onRunDone。transition 单独 try(M12)——并发 abort 导致
230
+ * illegal-transition 是预期的,可忽略。
231
+ */
232
+ async function finalizeTimeBudgetExhausted(run: WorkflowRun, deps: LifecycleDeps): Promise<void> {
233
+ deps.log?.("debug", "workflow:error-recovery", "time budget exhausted on rebuild, transition done", {
234
+ runId: run.runId,
235
+ budgetTimeMs: run.spec.budgetTimeMs,
236
+ });
237
+ run.state.error = run.state.error ?? `Time budget exhausted (${run.spec.budgetTimeMs} ms wall clock) before retry rebuild`;
238
+ let transitioned = false;
239
+ try {
240
+ run.transition("done", "time_limited");
241
+ transitioned = true;
242
+ } catch (te: unknown) {
243
+ // run 可能在检查后、transition 前被并发 abort——预期,不记错
244
+ void te;
245
+ }
246
+ if (!transitioned) return;
247
+ await deps.store.save(run).catch((e: unknown) => {
248
+ const m = e instanceof Error ? e.message : String(e);
249
+ logger.error(`[workflow] store.save failed (time budget exhausted): ${m}`);
250
+ });
251
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "time_limited" });
252
+ deps.onRunDone?.(run);
253
+ }
254
+
255
+ /**
256
+ * 重建整个 RunRuntime:新 controller + 新 worker。
257
+ *
258
+ * 调 run.replaceRuntime(newRt)(G5-001):原子释放旧 runtime(worker.terminate +
259
+ * abort)+ 绑定新 runtime,全程 status==="running" 不变(不变式 I1 不违反)。
260
+ *
261
+ * handlers 由调用方(lifecycle makeHandlers)构造——它们路由 onMessage/onError/
262
+ * onExit 回本文件的 handle* 函数。handlers 捕获 run + deps 闭包,runtime 重建后
263
+ * 仍有效(run 实例不变,deps 不变)。
264
+ *
265
+ * 前置:run.state.status === "running"(replaceRuntime 要求,G6-001)。
266
+ *
267
+ * [race-F3] 时间预算重排按剩余墙钟折算(remainingTimeBudgetMs),不再用满额——
268
+ * 否则每次错误重试都重置预算,最坏 6 次重试放大 ~6×。耗尽时的终态转移不在本函数
269
+ * (唯一生产调用方 scheduleRebuild 已前置拦截,见其注释)。
270
+ *
271
+ * @throws status !== "running"(由 replaceRuntime 抛)
272
+ */
273
+ export function rebuildRuntime(
274
+ run: WorkflowRun,
275
+ deps: LifecycleDeps,
276
+ handlers: WorkerHandlers,
277
+ ): void {
278
+ // OB3(可观察性):rebuild 关键节点 debug 日志——此前函数体 0 处 deps.log,
279
+ // 崩溃自愈只能靠行为证据诊断(L1 入口 / L2 重排 / L3 discard / L4 完成)。
280
+ deps.log?.("debug", "workflow:error-recovery", "runtime rebuild start", {
281
+ runId: run.runId,
282
+ budgetTimeMs: run.spec.budgetTimeMs,
283
+ });
284
+ const controller = new AbortController();
285
+ const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
286
+ // D-12 regression fix (round-2 #2):重新调度 run 级墙钟预算计时器。
287
+ // replaceRuntime 释放旧 runtime 时 clearTimeout 了旧计时器(run-runtime.release),
288
+ // 新 runtime 必须重排,否则带 budgetTimeMs 的 run 命中一次 worker/script 错误重试后
289
+ // 时间预算静默失效(直到 rebuildRuntime 才重排——本函数即唯一重排点)。
290
+ // deps.scheduleTimeBudget 由 Interface 层注入;未注入时(旧测试)跳过重排(兼容,
291
+ // 不影响无时间预算的 run)。
292
+ // 重排分支改为 if——语义与原三元一致(同一条件调 scheduleTimeBudget),仅为在
293
+ // 分支内记 L2 日志,控制流/异常语义零变化。
294
+ // [race-F3] 重排值改为剩余墙钟(remainingTimeBudgetMs)而非满额——重试不重置预算;
295
+ // L2 日志 payload 同步报实际重排值(排障时与 setTimeout 对得上)。remaining > 0
296
+ // 由调用方 scheduleRebuild 保证(耗尽在那里转 time_limited,不进本函数);本处
297
+ // remaining <= 0 时不挂 timer(防御直调,宁可不挂也不能挂出 0ms 立即触发)。
298
+ let timeBudgetTimer: ReturnType<typeof setTimeout> | undefined;
299
+ const remainingBudgetMs = remainingTimeBudgetMs(run);
300
+ if (remainingBudgetMs !== undefined && remainingBudgetMs > 0 && deps.scheduleTimeBudget) {
301
+ timeBudgetTimer = deps.scheduleTimeBudget(run.runId, remainingBudgetMs);
302
+ deps.log?.("debug", "workflow:error-recovery", "time budget rescheduled", {
303
+ runId: run.runId,
304
+ budgetTimeMs: remainingBudgetMs,
305
+ });
306
+ }
307
+ run.replaceRuntime(new RunRuntime(worker, controller, timeBudgetTimer));
308
+ // 清除被旧 runtime abort 的在飞 call——必须在 replaceRuntime 之后同步执行(无
309
+ // await 间隔):replaceRuntime 同步 abort 旧 controller + terminate 旧 worker,
310
+ // 在飞 executeAgentCall 的 finalize 发生在 `await runner.run` resolve 后的
311
+ // microtask,此刻在飞 call 仍为 "running"/"pending"(status !== "done")可精确
312
+ // 清理;genuinely-done 的 call 保留(重跑 replay)。放 delay 退避之前会误删退避
313
+ // 期间自然完成的真结果(重跑重复耗 token);放任何 await 之后,假失败已 finalize
314
+ // 为 "done" 挡不住——重跑 replay 会把 abort 错误当真结果回放,静默污染输出。
315
+ // 注意:discard 只清 Map/trace 条目,旧 executeAgentCall 的 promise 链仍会醒来
316
+ // finalize。markDone 在孤儿实例上无害,但后续投递并非 no-op——postAgentResult
317
+ // 会投给 run.runtime(已是新 worker)的同 callId pending,劫持重跑调用(实测
318
+ // S7-second 竞态:旧失败结果被 worker 侧 resolve 为空串 → 脚本假成功);
319
+ // finalizeCall 的 trace.update 在重跑已 append 同 stepIndex 新节点时命中新节点
320
+ // (瞬时污染,由重跑完成时的 update 覆盖)。该投递由 dispatchAgentCall 的
321
+ // 孤儿守卫(isOrphanedCall)拦截,trace.update 的瞬时污染由 executeAgentCall
322
+ // 的 isOrphaned 谓词(OB2)拦截,此处不重复设防。
323
+ const discardedCallIds = discardInFlightCalls(run);
324
+ deps.log?.("debug", "workflow:error-recovery", "in-flight calls discarded", {
325
+ runId: run.runId,
326
+ callIds: discardedCallIds,
327
+ count: discardedCallIds.length,
328
+ });
329
+ deps.log?.("debug", "workflow:error-recovery", "runtime rebuild complete", {
330
+ runId: run.runId,
331
+ });
332
+ }
333
+
334
+ // ── handleWorkerMessage(消息路由) ──────────────────────────
335
+
336
+ /**
337
+ * 路由 worker → main 的业务消息。
338
+ *
339
+ * agent_call → 派发 executeAgentCall(异步,不 await——立即返回让 worker 继续发消息)
340
+ * return → transition done,completed(脚本正常返回)
341
+ * error → handleScriptError(脚本主动抛错)
342
+ *
343
+ * 终态(done)下的 stale 消息丢弃(P0-1)。
344
+ */
345
+ export async function handleWorkerMessage(
346
+ run: WorkflowRun,
347
+ raw: unknown,
348
+ deps: LifecycleDeps,
349
+ handlers: WorkerHandlers,
350
+ ): Promise<void> {
351
+ // 终态(done)丢弃 stale 消息(P0-1)
352
+ if (isTerminal(run)) return;
353
+
354
+ // M7: 形状校验——防畸形 IPC 消息(worker 崩溃/发非对象)导致下游 TypeError
355
+ if (typeof raw !== "object" || raw === null) return;
356
+ const msg = raw as WorkerMsg;
357
+ switch (msg.type) {
358
+ case "agent-call":
359
+ dispatchAgentCall(run, msg, deps);
360
+ return;
361
+ case "workflow-call":
362
+ dispatchWorkflowCall(run, msg, deps);
363
+ return;
364
+ case "return":
365
+ // [F1] 标记本 runtime 代际已收到终态消息:WorkerHandle.isCurrent 守卫保证消息必
366
+ // 来自当前代际 worker。handleWorkerExit 的 exit(0) 无终态判定据此区分——
367
+ // 「已交付但 run 仍 running」(script-error 重试退避窗口)不得误判 failed。
368
+ if (run.runtime) run.runtime.receivedTerminalMessage = true;
369
+ await handleReturn(run, msg, deps);
370
+ return;
371
+ case "error":
372
+ // M1: 传 handlers(rebuildRuntime 需要)
373
+ // [F1] 同 return——error 也是终态消息,标记本代际已交付(同上防误判)。
374
+ if (run.runtime) run.runtime.receivedTerminalMessage = true;
375
+ await handleScriptError(
376
+ run,
377
+ msg.error,
378
+ msg.workerLogs ?? [],
379
+ deps,
380
+ handlers,
381
+ );
382
+ return;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 派发 agent 调用:构建 AgentCall + trace 节点,异步触发 executeAgentCall。
388
+ *
389
+ * 异步触发(不 await)——立即返回,让 worker 能继续发后续 agent-call(parallel 场景)。
390
+ * executeAgentCall 内部完成 markDone + trace.update。
391
+ *
392
+ * **C-3 修复**:executeAgentCall 经 dispatchCall 异步触发——原 gate.withSlot 包装已随
393
+ * 并发门闩 gate 抽象删除(no-op),并发调度归 SubagentService ConcurrencyPool,
394
+ * runner 管 spawn。
395
+ *
396
+ * **C-2 修复**:call 完成后检查 `budget.isExceeded` → abortRun(budget_limited),
397
+ * 终止整个 run(避免烧光预算后继续 spawn 新 call)。
398
+ *
399
+ * **stale 完成守卫(两层)**:completion 到达时——
400
+ * 1. `run.state.status === "running"` recheck:run 终止(abort/terminate)后到达的
401
+ * call 完成不写 run.state.calls / 不 postAgentResult(终态快照不被迟到结果污染);
402
+ * 2. 孤儿 call 实例比对(isOrphanedCall):rebuildRuntime 后旧代际 dispatch 的
403
+ * completion 不投递——rebuild 不改 status,第 1 层拦不住跨 runtime 代际的迟到
404
+ * 结果(S7-second 竞态:旧失败结果投给新 worker 劫持重跑 pending → 假成功)。
405
+ */
406
+ function dispatchAgentCall(
407
+ run: WorkflowRun,
408
+ msg: AgentCallMsg,
409
+ deps: LifecycleDeps,
410
+ ): void {
411
+ // M4: IPC 字段校验——畸形 agent-call 消息(opts 非对象/缺失、callId 非数字、prompt 缺失)
412
+ // 不写 trace / 不 postAgentResult——这类消息通常意味着 worker 模块版本不匹配或内存损坏,
413
+ // 回发结果给 worker 也没意义(worker 可能已崩)。仅记日志,让 worker timeout/exit 路径接管。
414
+ if (typeof msg.callId !== "number" || !Number.isFinite(msg.callId) ||
415
+ typeof msg.opts !== "object" || msg.opts === null ||
416
+ typeof msg.opts.prompt !== "string") {
417
+ logger.error(`[workflow] malformed agent-call message: callId=${JSON.stringify(msg.callId)}, opts=${JSON.stringify(msg.opts)?.slice(0, MALFORMED_MSG_LOG_PREVIEW_CHARS)}`);
418
+ return;
419
+ }
420
+
421
+ // 已缓存的调用直接 replay(跨 rebuild——崩溃重建后重跑脚本,已完成调用按 callId 命中缓存)
422
+ const cached = run.state.calls.get(msg.callId);
423
+ if (cached && cached.status === "done") {
424
+ postAgentResult(run, msg.callId, cached.result!, true);
425
+ return;
426
+ }
427
+
428
+ // 构建 trace 节点 + live record(TUI 实时进度)
429
+ const agentName = msg.opts.description ?? msg.opts.agent ?? "unknown";
430
+ // slug 复用 agentName(超长截断),live record 的 slug 仅用于 TUI 展示。
431
+ const liveSlug = agentName.length > SLUG_MAX_LENGTH ? agentName.slice(0, SLUG_MAX_LENGTH) : agentName;
432
+ const now = new Date().toISOString();
433
+ // live record:收口 agent 执行过程中的 text/thinking/toolCalls/usage,
434
+ // 供 TUI 在 agent 运行期间显示进度(getEventLog/getCurrentActivity)。
435
+ // 完成时由下方 .then 清除(终态由 node.result 承载)。
436
+ const liveRecord = createRecord(String(msg.callId), {
437
+ agent: agentName,
438
+ model: msg.opts.model ?? "default",
439
+ mode: "background",
440
+ task: msg.opts.prompt,
441
+ slug: liveSlug,
442
+ startedAt: Date.now(),
443
+ });
444
+ const node: ExecutionTraceNode = {
445
+ stepIndex: msg.callId,
446
+ agent: agentName,
447
+ task: msg.opts.prompt,
448
+ model: msg.opts.model ?? "default",
449
+ status: "running" as const,
450
+ phase: msg.phase,
451
+ startedAt: now,
452
+ live: liveRecord,
453
+ };
454
+ run.state.trace.append(node);
455
+
456
+ // 构建 AgentCall(opts 形状对齐 AgentCallOpts;schema: unknown → Record)
457
+ // 跨进程 IPC 边界的 schema 为 unknown,窄化前加 typeof guard 兜底。
458
+ const rawSchema = msg.opts.schema;
459
+ const opts: AgentCallOpts = {
460
+ ...msg.opts,
461
+ schema:
462
+ typeof rawSchema === "object" && rawSchema !== null
463
+ ? (rawSchema as Record<string, unknown>)
464
+ : undefined,
465
+ };
466
+
467
+ // BL-1:解析 skill/schema → skillPath / schemaEnv / appendSystemPrompt。
468
+ // M2 修正后 resolveAgentOpts 单参数,只处理 schema SO 指令(内容直传)+ skill。
469
+ // agent ref 处理(systemPrompt/model/thinkingLevel)交 resolveIdentity(经
470
+ // getAgentConfig + resolveModel 完整覆盖),消除双重注入与 model 层级混乱。
471
+ // 解析失败(skill 未找到)走 error 路径,不发 slot、不 spawn。
472
+ const resolved = resolveAgentOpts(opts);
473
+ if (resolved.error) {
474
+ const call = new AgentCall(msg.callId, opts, node);
475
+ call.markRunning();
476
+ const errorResult: AgentResult = { content: "", error: resolved.error };
477
+ call.markDone(errorResult);
478
+ run.state.calls.set(msg.callId, call);
479
+ // 无子进程执行,清除空 live record(终态由 result 承载)
480
+ node.live = undefined;
481
+ run.state.trace.update(msg.callId, {
482
+ status: "failed",
483
+ result: errorResult,
484
+ completedAt: new Date().toISOString(),
485
+ });
486
+ postAgentResult(run, msg.callId, errorResult, false);
487
+ deps.store.save(run).catch((e: unknown) => {
488
+ logger.error(`[workflow] store.save failed (resolveAgentOpts): ${e instanceof Error ? e.message : String(e)}`);
489
+ });
490
+ return;
491
+ }
492
+
493
+ const call = new AgentCall(msg.callId, resolved.opts, node);
494
+ run.state.calls.set(msg.callId, call);
495
+
496
+ // C-3:agent call 执行入口。
497
+ // (原经 gate.withSlot 包装,并发门闩 gate 已删——no-op 抽象,实际并发由
498
+ // SubagentService ConcurrencyPool 管理;仅保留其 pre-abort 检查语义,见下方
499
+ // dispatchCall 内 signal.aborted 分支。)executeAgentCall 管 retry/budget/stale-context;
500
+ // runner(runner.run)管 spawn pi 子进程。
501
+ // assignRuntime/replaceRuntime 保证 status==="running" ⟺ runtime defined,
502
+ // 故 run.runtime 在此必存在(dispatchAgentCall 仅从 handleWorkerMessage 调用,
503
+ // 后者已守 terminal(isTerminal)早期 return)。fallback new AbortController 已移除。
504
+ const runtime = run.runtime!;
505
+ const signal = runtime.controller.signal;
506
+ // D-005: onEvent 签名升级——executeAndAwait 直接出 AgentEvent(强类型,
507
+ // session-runner handleSdkEvent 出口),不再有 raw JSONL 中间层。
508
+ // 删 jsonlToAgentEvent 翻译——直接 updateFromEvent。
509
+ // TUI 靠 tick 轮询 trace.toArray() 读 node.live,无需显式通知。
510
+ const onEvent = (event: AgentEvent): void => {
511
+ updateFromEvent(liveRecord, event);
512
+ };
513
+ // 创建 streaming sink:widgetKey = subagent-stream-<runId>-<stepIndex>。
514
+ // 复用 background subagent 的 SubagentStream → setWidget → RPC 链路(agent-call-streaming-extension.md)。
515
+ // streamSink 缺失(无 UI 模式)时 stream=undefined,executeAgentCall 正常执行不 streaming。
516
+ const stream = deps.streamSink
517
+ ? new SubagentStream(`${run.runId}-${msg.callId}`, deps.streamSink)
518
+ : undefined;
519
+ // 原 gate.withSlot(fn, signal) 语义内联:pre-aborted 时 reject AbortError(
520
+ // 下方 .catch 依赖此约定不记错),否则直接执行——并发调度归 ConcurrencyPool。
521
+ const dispatchCall = async (): Promise<void> => {
522
+ if (signal.aborted) {
523
+ const abortErr = new Error("Operation aborted before start");
524
+ abortErr.name = "AbortError";
525
+ throw abortErr;
526
+ }
527
+ try {
528
+ // OB2(S7 残留):isOrphaned 谓词注入——旧代际 finalize 在 trace.update 前被
529
+ // 拦截(判定语义与下方 .then/.catch 守卫同一 isOrphanedCall,详见
530
+ // execute-agent-call.ts finalizeCall 文档注释)。
531
+ await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace, onEvent, stream, () => isOrphanedCall(run, msg.callId, call));
532
+ } finally {
533
+ stream?.dispose();
534
+ }
535
+ };
536
+ void dispatchCall()
537
+ .then(() => {
538
+ // 清除 live record:终态已由 executeAgentCall → finalizeCall 写入 node.result,
539
+ // live 不再需要(且含可变状态,不保留)。无论 stale 与否都清,避免内存泄漏。
540
+ // M4: 必须在 stale guard 之前清,否则跨 rebuild 的迟到 completion 会累积 live record。
541
+ node.live = undefined;
542
+ // run 终止(终态)后到达的 stale completion 不写 state
543
+ if (run.state.status !== "running") return;
544
+ // 孤儿 call 守卫(S7-second 竞态):rebuild 的 discardInFlightCalls 已移除本
545
+ // call、或重跑 dispatch 已用新实例替换同 callId 条目时,本 completion 属于旧
546
+ // runtime 代际。postAgentResult 的投递目标是 run.runtime(已是新 worker),
547
+ // 迟到结果会劫持新 worker 内重跑 agent() 的 pending Promise——跳过投递 /
548
+ // budget 同步 / 持久化,仅留日志。executeAgentCall 内 finalizeCall 的
549
+ // trace.update 若已命中重跑新节点(瞬时污染),由重跑完成时的 update 覆盖。
550
+ if (isOrphanedCall(run, msg.callId, call)) {
551
+ deps.log?.("debug", "workflow:error-recovery", "orphan agent call completion dropped", { runId: run.runId, callId: msg.callId });
552
+ return;
553
+ }
554
+ if (call.result) postAgentResult(run, msg.callId, call.result, false);
555
+ // D-12 regression fix (round-2 #1):executeAgentCall 内 consume/incrementCallCount
556
+ // 后同步 worker $BUDGET(否则 $BUDGET.spent()/remaining() 恒为 0)
557
+ postBudgetUpdate(run);
558
+ deps.store.save(run).catch((e: unknown) => {
559
+ const m = e instanceof Error ? e.message : String(e);
560
+ logger.error(`[workflow] store.save failed (agent call ${msg.callId}): ${m}`);
561
+ });
562
+
563
+ // C-2:budget 超限 → 终止整个 run(避免继续 spawn 烧预算)
564
+ // 内联 terminate(不调 lifecycle.abortRun 避免 engine 内循环依赖):
565
+ // 若 run 仍非终态,transition done,budget_limited + 持久化。
566
+ // 上方 status !== "running" 已保证此处非 done(且 transition 内含 done no-op 守卫)。
567
+ if (run.state.budget.isExceeded()) {
568
+ run.state.error = run.state.error ?? "Budget exceeded";
569
+ deps.log?.("debug", "workflow:error-recovery", "budget exceeded, transition done", { runId: run.runId });
570
+ // M12: transition 单独 try——并发 abort 导致 illegal-transition 是预期的,可忽略
571
+ let transitioned = false;
572
+ try {
573
+ run.transition("done", "budget_limited");
574
+ transitioned = true;
575
+ } catch (te: unknown) {
576
+ // run 可能在 budget 检查后、transition 前被并发 abort——预期,不记错
577
+ void te;
578
+ }
579
+ if (transitioned) {
580
+ deps.store.save(run).catch((e: unknown) => {
581
+ const m = e instanceof Error ? e.message : String(e);
582
+ logger.error(`[workflow] store.save failed (budget done): ${m}`);
583
+ });
584
+ deps.log?.("debug", "workflow:error-recovery", "run saved after budget done", { runId: run.runId, reason: run.state.reason });
585
+ // M12: onRunDone/emit 单独 try——这些是真实副作用,错误不应被静默吞掉
586
+ try {
587
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
588
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
589
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
590
+ deps.onRunDone?.(run);
591
+ } catch (err) {
592
+ const m = err instanceof Error ? err.message : String(err);
593
+ logger.error(`[workflow] onRunDone/emit failed (budget done): ${m}`);
594
+ }
595
+ }
596
+ }
597
+ })
598
+ .catch((err: unknown) => {
599
+ // pre-abort 检查(原 gate.withSlot 语义)在 dispatchCall 入口 reject AbortError——预期,不记错。
600
+ if (err instanceof Error && err.name === "AbortError") return;
601
+ const message = err instanceof Error ? err.message : String(err);
602
+ logger.error(`[workflow] agent call ${msg.callId} failed: ${message}`);
603
+ // 兜底回发:executeAgentCall 抛非 Abort 异常时(如 runner undefined 的 TypeError、
604
+ // dispatchCall 内部 bug)原 catch 仅 console.error,worker 内对 callId 的 pending
605
+ // Promise 永不 resolve → agent() 永久 await → worker 脚本挂死。构造 failed AgentResult
606
+ //(与 resolveAgentOpts 失败路径 L262-275 一致的模式)postAgentResult 回 worker,
607
+ // 让 pending Promise resolve(结果为 error),脚本可继续或失败退出。
608
+ // 孤儿 call 守卫(与 .then 对称,S7-second 竞态):rebuild 后本 call 已被 discard
609
+ // 移除/替换——markDone 虽在孤儿实例上无害,但 trace.update 会污染重跑新建的同
610
+ // stepIndex 节点、postAgentResult 会劫持新 worker 的同 callId pending。孤儿时只
611
+ // 留日志,全部跳过。node.live 无条件先清(旧节点已脱离 trace,防御性统一)。
612
+ node.live = undefined;
613
+ if (isOrphanedCall(run, msg.callId, call)) {
614
+ deps.log?.("debug", "workflow:error-recovery", "orphan agent call failure dropped", { runId: run.runId, callId: msg.callId });
615
+ return;
616
+ }
617
+ const errorResult: AgentResult = { content: "", error: message };
618
+ // call 已 done(executeAgentCall 内 finalizeCall 已 markDone)时跳过,避免重复 markDone。
619
+ // status 理论上必为 running(executeAgentCall L130 markRunning 先于 reject),pending
620
+ // 分支为防御性保护。非 running/done 意外态:跳过 markDone(markDone 要求 running)。
621
+ if (call.status !== "done") {
622
+ if (call.status === "pending") call.markRunning();
623
+ call.markDone(errorResult);
624
+ }
625
+ // state 一致性三件套(与 resolveAgentOpts 失败 L268-276 / .then L319-325 对等):
626
+ // trace 标 failed + 清 live record(防泄漏)+ 持久化(catch 恰是最需留证的场景)。
627
+ // stale 终态(run 已 done)时 run.runtime 为 undefined,postAgentResult 用
628
+ // optional chaining 跳过 worker 回发;trace/state 写入仍执行(无害,终态快照已存)。
629
+ run.state.trace.update(msg.callId, {
630
+ status: "failed",
631
+ result: errorResult,
632
+ completedAt: new Date().toISOString(),
633
+ });
634
+ postAgentResult(run, msg.callId, errorResult, false);
635
+ // S2: 与 .then 对称——catch 路径也同步 worker $BUDGET(幂等)
636
+ postBudgetUpdate(run);
637
+ deps.store.save(run).catch((e: unknown) => {
638
+ logger.error(`[workflow] store.save failed (catch fallback): ${e instanceof Error ? e.message : String(e)}`);
639
+ });
640
+ });
641
+ }
642
+
643
+ /**
644
+ * postMessage 序列化失败时回发的 fallback result(必可克隆),让 worker pending resolve。
645
+ *
646
+ * postResult(workflow-call)与 postAgentResult(agent-call)各自前缀不同,故 prefix 参数化,
647
+ * 共享返回类型与构造逻辑,避免字面量重复导致形状漂移。
648
+ *
649
+ * W2 防御关键纯函数——export 供独立单测(error-recovery-serialize-failed-result.test.ts)验证
650
+ * 返回 shape `{content:"", error:"<prefix>: <errMsg>"}`,确保两条 fallback 路径(workflow-call /
651
+ * agent-call)共享同一构造逻辑不漂移。
652
+ */
653
+ export function makeSerializeFailedResult(
654
+ prefix: string,
655
+ errMsg: string,
656
+ ): { content: string; error: string } {
657
+ return { content: "", error: `${prefix}: ${errMsg}` };
658
+ }
659
+
660
+ /**
661
+ * 派发 workflow 嵌套调用:调 deps.onWorkflowCall 获取子 workflow 结果,
662
+ * 异步 postMessage(workflow-result) 回 worker。
663
+ *
664
+ * onWorkflowCall 未注入时(向后兼容),返回 error result 让脚本 soft-fail。
665
+ * 与 dispatchAgentCall 对称:异步触发(不 await),stale 完成守卫(终态不发)。
666
+ */
667
+ function dispatchWorkflowCall(
668
+ run: WorkflowRun,
669
+ msg: WorkflowCallMsg,
670
+ deps: LifecycleDeps,
671
+ ): void {
672
+ // M4: IPC 字段校验——畸形 workflow-call 消息
673
+ if (typeof msg.callId !== "number" || !Number.isFinite(msg.callId) ||
674
+ typeof msg.name !== "string" ||
675
+ typeof msg.args !== "object" || msg.args === null) {
676
+ logger.error(`[workflow] malformed workflow-call message: callId=${JSON.stringify(msg.callId)}, name=${JSON.stringify(msg.name)}`);
677
+ return;
678
+ }
679
+
680
+ const postResult = (result: unknown): void => {
681
+ if (run.state.status !== "running") return;
682
+ // W2 主线程防御:result 是子 workflow 任意返回值,可能含不可克隆成员(function/
683
+ // Symbol/循环引用)→ postMessage 同步抛 DataCloneError。内部 try/catch + 回发
684
+ // 纯字符串 fallback result,让 worker 内 workflow() pending Promise resolve。
685
+ // 注意:错误变量用 err(外层 dispatchWorkflowCall 参数名为 msg,避免遮蔽)。
686
+ try {
687
+ run.runtime?.worker.postMessage({
688
+ type: "workflow-result",
689
+ callId: msg.callId,
690
+ result,
691
+ });
692
+ } catch (err) {
693
+ const errMsg = err instanceof Error ? err.message : String(err);
694
+ logger.error(`[workflow] postResult (workflow-call callId=${msg.callId}) failed: ${errMsg}. Sending error fallback.`);
695
+ // 回发纯字符串 fallback result(必可克隆),让 worker pending resolve
696
+ try {
697
+ run.runtime?.worker.postMessage({
698
+ type: "workflow-result",
699
+ callId: msg.callId,
700
+ result: makeSerializeFailedResult("Workflow result serialization failed", errMsg),
701
+ });
702
+ } catch {
703
+ // fallback 也失败——worker 此 callId 的 pending 只能靠 timeout 兜底
704
+ logger.error(`[workflow] postResult fallback also failed (callId=${msg.callId}): worker pending will hang until timeout`);
705
+ }
706
+ }
707
+ };
708
+
709
+ if (!deps.onWorkflowCall) {
710
+ postResult({
711
+ content: "",
712
+ error: `workflow() not supported: onWorkflowCall not injected`,
713
+ });
714
+ return;
715
+ }
716
+
717
+ void deps
718
+ .onWorkflowCall(msg.name, msg.args, run)
719
+ .then(postResult)
720
+ .catch((err: unknown) => {
721
+ postResult({
722
+ content: "",
723
+ error: err instanceof Error ? err.message : String(err),
724
+ });
725
+ });
726
+ }
727
+
728
+ /**
729
+ * 回发 agent-result 给 worker(worker 内 pending Promise 据此 resolve)。
730
+ *
731
+ * W2 主线程防御:result 是 agent 返回值,含不可克隆成员(function/Symbol/循环引用)时
732
+ * postMessage 同步抛 DataCloneError。若冒泡到 dispatchAgentCall 的 .then 回调,会中断
733
+ * 后续 postBudgetUpdate/store.save/budget 检查,run 卡在 running。故内部 try/catch:
734
+ * 失败时记录诊断 + 回发纯字符串 fallback result(必可克隆),让 worker pending resolve。
735
+ * 函数签名不变(所有调用点无需改动),仅用共享 logger 记日志(deps 不在手边)。
736
+ */
737
+ function postAgentResult(
738
+ run: WorkflowRun,
739
+ callId: number,
740
+ result: AgentResult,
741
+ cached: boolean,
742
+ ): void {
743
+ try {
744
+ run.runtime?.worker.postMessage({ type: "agent-result", callId, result, cached });
745
+ } catch (err) {
746
+ const msg = err instanceof Error ? err.message : String(err);
747
+ logger.error(`[workflow] postAgentResult failed (callId=${callId}): ${msg}. Result likely contains non-cloneable value.`);
748
+ // 回发纯字符串 fallback result(必可克隆),让 worker pending resolve(避免永久挂起)
749
+ try {
750
+ run.runtime?.worker.postMessage({
751
+ type: "agent-result",
752
+ callId,
753
+ result: makeSerializeFailedResult("Result serialization failed", msg),
754
+ // 原 result 不可克隆时 cached 透传原值含义失真(fallback result 非缓存命中)→ 固定 false
755
+ cached: false,
756
+ });
757
+ } catch {
758
+ // fallback 也失败——worker 此 callId 的 pending 只能靠 timeout/exit 兜底
759
+ logger.error(`[workflow] postAgentResult fallback also failed (callId=${callId}): worker pending will hang until timeout`);
760
+ }
761
+ }
762
+ }
763
+
764
+ /**
765
+ * 回发 budget-update 给 worker($BUDGET 据 worker-script-builder 的 budget-update 分支
766
+ * 更新 spent()/remaining())。每次 agent 调用消费 usage 后发送,保持 worker 内 $BUDGET
767
+ * 与主线程 Budget 值对象同步。
768
+ *
769
+ * D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery 主路径调用
770
+ * (dispatch 后同步 worker $BUDGET)——单一实现,避免消息形状漂移。
771
+ */
772
+ export function postBudgetUpdate(run: WorkflowRun): void {
773
+ try {
774
+ run.runtime?.worker.postMessage({
775
+ type: "budget-update",
776
+ budget: {
777
+ usedTokens: run.state.budget.usedTokens,
778
+ usedCost: run.state.budget.usedCost,
779
+ },
780
+ });
781
+ } catch (err) {
782
+ const msg = err instanceof Error ? err.message : String(err);
783
+ // budget 是纯 number 不太可能失败,但防御性兜底——budget 同步非关键(worker 仍可
784
+ // 基于 $BUDGET.spent() 自行累计),失败仅记日志,不中断调用方流程。
785
+ logger.error(`[workflow] postBudgetUpdate failed: ${msg}. Budget sync to worker skipped (non-critical).`);
786
+ }
787
+ }
788
+
789
+ /**
790
+ * 处理脚本的 return 消息:transition done,completed + 持久化。
791
+ */
792
+ async function handleReturn(
793
+ run: WorkflowRun,
794
+ msg: ReturnMsg,
795
+ deps: LifecycleDeps,
796
+ ): Promise<void> {
797
+ deps.log?.("debug", "workflow:error-recovery", "handleReturn", { runId: run.runId, status: run.state.status });
798
+ // 捕获 worker 诊断日志(P2-2)
799
+ // L9: 追加而非覆盖——保留重试历史的诊断日志(各 worker 实例的 console 输出)
800
+ if (msg.workerLogs && msg.workerLogs.length > 0) {
801
+ run.state.errorLogs.push(...msg.workerLogs);
802
+ if (run.state.errorLogs.length > MAX_ERROR_LOGS) {
803
+ run.state.errorLogs = run.state.errorLogs.slice(-MAX_ERROR_LOGS);
804
+ }
805
+ }
806
+ run.state.scriptResult = msg.result;
807
+ run.transition("done", "completed");
808
+ // [SW-DATA-3] save 失败不阻断终态推进(原 await 裸抛 → unhandledRejection + 幽灵注销)
809
+ await saveRunBestEffort(run, deps, "handleReturn (done,completed)");
810
+ deps.log?.("debug", "workflow:error-recovery", "run saved after return", { runId: run.runId, reason: run.state.reason });
811
+ // C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
812
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
813
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
814
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
815
+ deps.onRunDone?.(run);
816
+ }
817
+
818
+ // ── handleWorkerError ────────────────────────────────────────
819
+
820
+ /**
821
+ * 处理 worker 线程 uncaught error。
822
+ *
823
+ * 重试矩阵(domain-models.md §失败处理矩阵):
824
+ * - run.meta.workerErrorCount(C.5,跨 runtime 存活)< MAX → 退避 + rebuildRuntime
825
+ * - >= MAX → transition done,failed
826
+ *
827
+ * [R4-F1] 同代际幂等:复用 receivedTerminalMessage 代际标志(见函数体注释)——
828
+ * worker 崩溃时 error + exit(1) 双事件只处理一次(第二个事件直接跳过)。
829
+ *
830
+ * @throws 不抛错——所有失败路径转 transition 或日志
831
+ */
832
+ export async function handleWorkerError(
833
+ run: WorkflowRun,
834
+ err: Error,
835
+ deps: LifecycleDeps,
836
+ handlers: WorkerHandlers,
837
+ ): Promise<void> {
838
+ // 与 handleWorkerMessage 对称——终态(done)丢弃 stale error。
839
+ // 否则终态后到达的 worker error 仍会 workerErrorCount++(污染跨 runtime 计数)。
840
+ if (isTerminal(run)) return;
841
+
842
+ // [R4-F1] 同代际幂等守卫:worker 崩溃时 error + exit(1) 双事件各派发一次
843
+ // handleWorkerError(onError 先到,exit 非 0 经 handleWorkerExit 委托二次到达)——
844
+ // 旧实现单次崩溃 workerErrorCount +2、两个 scheduleRebuild 并行交错(双 rebuild
845
+ // 各自 new Worker,旧 handle 的 terminate/exit 事件与新 handle 的生命周期互相踩踏)。
846
+ // 复用 R4 的 receivedTerminalMessage 代际标志(RunRuntime 字段,rebuild 自然重置):
847
+ // 进入处理前置 true 标记「本代际已有 error/terminal 处理」,第二个事件(无论
848
+ // onError 直达还是 exit(1) 委托)命中标志直接跳过。新代际的 handleWorkerError
849
+ // 不受影响(新 RunRuntime 的标志为 false)。
850
+ if (run.runtime?.receivedTerminalMessage) return;
851
+ if (run.runtime) run.runtime.receivedTerminalMessage = true;
852
+
853
+ const count = (run.meta.workerErrorCount ?? 0) + 1;
854
+ run.meta.workerErrorCount = count;
855
+
856
+ if (count <= MAX_WORKER_RETRIES) {
857
+ await scheduleRebuild(run, deps, handlers);
858
+ return;
859
+ }
860
+
861
+ // 超限 → failed
862
+ run.state.error = err.message;
863
+ deps.log?.("debug", "workflow:error-recovery", "handleWorkerError retries exceeded, transition done", { runId: run.runId, count });
864
+ run.transition("done", "failed");
865
+ // [SW-DATA-3] save 失败不阻断终态推进
866
+ await saveRunBestEffort(run, deps, "handleWorkerError (done,failed)");
867
+ deps.log?.("debug", "workflow:error-recovery", "run saved after worker error", { runId: run.runId, reason: run.state.reason });
868
+ // C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
869
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
870
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
871
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
872
+ deps.onRunDone?.(run);
873
+ }
874
+
875
+ // ── handleWorkerExit ─────────────────────────────────────────
876
+
877
+ /**
878
+ * 处理 worker 线程 exit。
879
+ *
880
+ * code === 0:
881
+ * - 本代际已收到终态消息(return/error)→ no-op(正常收尾退出,或 script-error 重试
882
+ * 退避窗口——rebuild 即将发生,不得干扰)
883
+ * - 本代际未收到任何终态消息 → [F1] 转 done,failed(WORKER_EXITED_WITHOUT_RESULT_MSG)。
884
+ * 旧实现对 code===0 一律 no-op:不可克隆 return 被 worker 侧 _safePost 吞掉后
885
+ * DataCloneError 静默丢失,worker exit(0) 而 run 永久 running、runAndWait 悬挂。
886
+ * code !== 0 → 委托 handleWorkerError(非零 exit 视为崩溃,既有重试矩阵;重试耗尽仍会
887
+ * 转 done,failed,无悬挂面)
888
+ *
889
+ * **G-025 竞态防护**:检查 handle.isCurrent——stale exit 事件(已 terminate 的旧
890
+ * worker 的 exit)直接丢弃,不影响当前 runtime 的新 worker。
891
+ */
892
+ export async function handleWorkerExit(
893
+ run: WorkflowRun,
894
+ code: number,
895
+ handle: WorkerHandle,
896
+ deps: LifecycleDeps,
897
+ handlers: WorkerHandlers,
898
+ ): Promise<void> {
899
+ // G-025: stale exit 事件丢弃(handle 已不是当前 runtime 的 worker)
900
+ if (!handle.isCurrent) return;
901
+ if (isTerminal(run)) return;
902
+
903
+ if (code === 0) {
904
+ // 本代际已交付终态消息 → 正常收尾 / 重试退避窗口,no-op(rebuild 负责后续)
905
+ if (run.runtime?.receivedTerminalMessage) return;
906
+
907
+ // [F1] 无终态消息的 exit(0) = worker 静默退出(不可克隆 return 被吞 / 脚本直调
908
+ // process.exit(0) 等)。置 failed 保证 runAndWait 必有终态。不重试:rebuild 重跑
909
+ // 脚本对确定性根因(不可克隆 return)无意义,且 belt 路径优先给用户明确归因。
910
+ deps.log?.("debug", "workflow:error-recovery", "worker exited without terminal message, transition done", { runId: run.runId });
911
+ run.state.error = WORKER_EXITED_WITHOUT_RESULT_MSG;
912
+ run.transition("done", "failed");
913
+ await saveRunBestEffort(run, deps, "handleWorkerExit (done,failed, no terminal message)");
914
+ deps.log?.("debug", "workflow:error-recovery", "run saved after exit without result", { runId: run.runId, reason: run.state.reason });
915
+ // C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
916
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
917
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
918
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
919
+ deps.onRunDone?.(run);
920
+ return;
921
+ }
922
+
923
+ // 非零 exit → 委托 handleWorkerError(C.3: onExit 传 handle 用于竞态防护)
924
+ await handleWorkerError(
925
+ run,
926
+ new Error(`Worker exited with code ${code}`),
927
+ deps,
928
+ handlers,
929
+ );
930
+ }
931
+
932
+ // ── handleScriptError ────────────────────────────────────────
933
+
934
+ /**
935
+ * 处理脚本主动抛出的 error(type:"error" from worker)。
936
+ *
937
+ * 重试矩阵:
938
+ * - run.meta.scriptErrorCount(C.5)< MAX → 退避 + rebuildRuntime(N2: 补全重建)
939
+ * - >= MAX → transition done,failed
940
+ *
941
+ * @param workerLogs worker console.* 捕获(P2-2,存 run.state.errorLogs 供 TUI 展示)
942
+ */
943
+ export async function handleScriptError(
944
+ run: WorkflowRun,
945
+ errorMsg: string,
946
+ workerLogs: WorkerLogEntry[],
947
+ deps: LifecycleDeps,
948
+ handlers: WorkerHandlers,
949
+ ): Promise<void> {
950
+ // 与 handleWorkerMessage/handleWorkerError 对称——终态守卫前置。
951
+ if (isTerminal(run)) return;
952
+
953
+ // P2-2: 捕获 worker 诊断日志
954
+ // L9: 追加而非覆盖
955
+ if (workerLogs.length > 0) {
956
+ run.state.errorLogs.push(...workerLogs);
957
+ if (run.state.errorLogs.length > MAX_ERROR_LOGS) {
958
+ run.state.errorLogs = run.state.errorLogs.slice(-MAX_ERROR_LOGS);
959
+ }
960
+ }
961
+
962
+ const count = (run.meta.scriptErrorCount ?? 0) + 1;
963
+ run.meta.scriptErrorCount = count;
964
+
965
+ if (count <= MAX_WORKER_RETRIES) {
966
+ await scheduleRebuild(run, deps, handlers);
967
+ return;
968
+ }
969
+
970
+ // 超限 → failed
971
+ run.state.error = `Workflow failed after ${MAX_WORKER_RETRIES} retries: ${errorMsg}`;
972
+ deps.log?.("debug", "workflow:error-recovery", "handleScriptError retries exceeded, transition done", { runId: run.runId, count });
973
+ run.transition("done", "failed");
974
+ // [SW-DATA-3] save 失败不阻断终态推进
975
+ await saveRunBestEffort(run, deps, "handleScriptError (done,failed)");
976
+ deps.log?.("debug", "workflow:error-recovery", "run saved after script error", { runId: run.runId, reason: run.state.reason });
977
+ // C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
978
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
979
+ deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
980
+ deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
981
+ deps.onRunDone?.(run);
982
+ }
983
+
984
+ // ── scheduleRebuild(退避 + 重建) ──────────────────────────
985
+
986
+ /**
987
+ * 退避后重建 RunRuntime(G3-001 整重建)。
988
+ *
989
+ * 退避期间 run 可能被 abort(转终态 done)——rebuildRuntime 前重检状态,终态时
990
+ * 跳过重建(避免给已终止的 run 启新 worker)。
991
+ */
992
+ async function scheduleRebuild(
993
+ run: WorkflowRun,
994
+ deps: LifecycleDeps,
995
+ handlers: WorkerHandlers,
996
+ ): Promise<void> {
997
+ // 用当前重试计数算退避(workerErrorCount 或 scriptErrorCount 已递增)
998
+ const retryIndex = Math.max(
999
+ run.meta.workerErrorCount ?? 0,
1000
+ run.meta.scriptErrorCount ?? 0,
1001
+ );
1002
+ await delay(backoffDelay(retryIndex));
1003
+
1004
+ // 退避期间状态可能变化——重检
1005
+ if (isTerminal(run)) return;
1006
+
1007
+ // [race-F3] 时间预算折算后已耗尽 → 不再 rebuild 重试,直接 time_limited 终态。
1008
+ // 必须在退避 delay 之后、rebuildRuntime 之前检查:检查前移会在「退避期间耗尽」的
1009
+ // 窗口漏判(rebuild 挂不出 timer,run 预算静默失效);检查点与 rebuildRuntime 的
1010
+ // 计时器挂载之间无 await,remaining > 0 判定不会失效。
1011
+ const remainingMs = remainingTimeBudgetMs(run);
1012
+ if (remainingMs !== undefined && remainingMs <= 0) {
1013
+ await finalizeTimeBudgetExhausted(run, deps);
1014
+ return;
1015
+ }
1016
+
1017
+ rebuildRuntime(run, deps, handlers);
1018
+ }