@selesai/code 0.13.15 → 0.13.18

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 (473) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -14
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.js +3 -9
  5. package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
  6. package/dist/bun/sandbox-env-setup.d.ts +1 -0
  7. package/dist/bun/sandbox-env-setup.js +3 -0
  8. package/dist/cli/auth-command.d.ts +23 -0
  9. package/dist/cli/auth-command.js +102 -0
  10. package/dist/cli/config-selector.js +2 -1
  11. package/dist/cli/setup.d.ts +1 -0
  12. package/dist/cli/setup.js +11 -0
  13. package/dist/cli.js +2 -14
  14. package/dist/config.d.ts +3 -2
  15. package/dist/config.js +9 -4
  16. package/dist/core/agent-session-runtime.js +15 -7
  17. package/dist/core/agent-session.d.ts +2 -6
  18. package/dist/core/agent-session.js +15 -4
  19. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  20. package/dist/core/compaction/branch-summarization.js +2 -1
  21. package/dist/core/extensions/loader.js +3 -0
  22. package/dist/core/extensions/types.d.ts +16 -2
  23. package/dist/core/http-dispatcher.js +2 -0
  24. package/dist/core/keybindings.d.ts +5 -0
  25. package/dist/core/keybindings.js +4 -0
  26. package/dist/core/messages.d.ts +1 -1
  27. package/dist/core/model-config.d.ts +15 -0
  28. package/dist/core/model-config.js +3 -0
  29. package/dist/core/model-runtime.d.ts +1 -0
  30. package/dist/core/model-runtime.js +5 -2
  31. package/dist/core/provider-composer.js +8 -2
  32. package/dist/core/session-export.d.ts +3 -0
  33. package/dist/core/session-export.js +31 -0
  34. package/dist/core/session-manager.d.ts +3 -2
  35. package/dist/core/session-manager.js +40 -13
  36. package/dist/core/skills.d.ts +1 -1
  37. package/dist/core/skills.js +4 -2
  38. package/dist/core/system-prompt.js +10 -10
  39. package/dist/core/tools/bash.js +3 -117
  40. package/dist/core/tools/edit.d.ts +1 -12
  41. package/dist/core/tools/edit.js +5 -163
  42. package/dist/core/tools/find.js +4 -55
  43. package/dist/core/tools/grep.js +4 -60
  44. package/dist/core/tools/ls.js +4 -49
  45. package/dist/core/tools/read.d.ts +4 -0
  46. package/dist/core/tools/read.js +10 -122
  47. package/dist/core/tools/renderers/bash.d.ts +11 -0
  48. package/dist/core/tools/renderers/bash.js +129 -0
  49. package/dist/core/tools/renderers/edit.d.ts +22 -0
  50. package/dist/core/tools/renderers/edit.js +166 -0
  51. package/dist/core/tools/renderers/find.d.ts +9 -0
  52. package/dist/core/tools/renderers/find.js +63 -0
  53. package/dist/core/tools/renderers/grep.d.ts +9 -0
  54. package/dist/core/tools/renderers/grep.js +68 -0
  55. package/dist/core/tools/renderers/index.d.ts +27 -0
  56. package/dist/core/tools/renderers/index.js +46 -0
  57. package/dist/core/tools/renderers/ls.d.ts +9 -0
  58. package/dist/core/tools/renderers/ls.js +57 -0
  59. package/dist/core/tools/renderers/read.d.ts +10 -0
  60. package/dist/core/tools/renderers/read.js +128 -0
  61. package/dist/core/tools/renderers/write.d.ts +9 -0
  62. package/dist/core/tools/renderers/write.js +152 -0
  63. package/dist/core/tools/write.js +5 -146
  64. package/dist/extensions/capability-gateway/index.ts +12 -1
  65. package/dist/extensions/capability-gateway/integration.test.ts +23 -0
  66. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
  67. package/dist/extensions/pi-hermes-memory/README.md +16 -12
  68. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
  69. package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
  70. package/dist/extensions/pi-hermes-memory/package.json +4 -4
  71. package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
  72. package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
  73. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
  74. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
  75. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
  76. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
  77. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
  78. package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
  79. package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
  80. package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
  82. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
  84. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
  85. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
  86. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
  87. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
  88. package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
  89. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
  90. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
  91. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
  92. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
  93. package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
  94. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
  95. package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
  96. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
  97. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
  98. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
  99. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
  100. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
  101. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
  102. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
  103. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  104. package/dist/extensions/pi-intercom/README.md +16 -2
  105. package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
  106. package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
  107. package/dist/extensions/pi-intercom/index.ts +1 -1
  108. package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
  109. package/dist/extensions/pi-intercom/package.json +1 -1
  110. package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
  111. package/dist/extensions/pi-intercom/project-agent.ts +1 -6
  112. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
  113. package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
  114. package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
  115. package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
  116. package/dist/extensions/pi-subagents/README.md +3 -3
  117. package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
  118. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
  119. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  120. package/dist/extensions/pi-subagents/docs/agents.md +32 -16
  121. package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
  122. package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
  123. package/dist/extensions/pi-subagents/docs/missions.md +2 -0
  124. package/dist/extensions/pi-subagents/docs/models.md +58 -1
  125. package/dist/extensions/pi-subagents/docs/observability.md +50 -12
  126. package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
  127. package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
  128. package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
  129. package/dist/extensions/pi-subagents/package-lock.json +747 -177
  130. package/dist/extensions/pi-subagents/package.json +6 -4
  131. package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
  132. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
  133. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  137. package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
  138. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
  139. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  140. package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
  141. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
  142. package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  143. package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
  144. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
  145. package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
  146. package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
  147. package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
  148. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
  149. package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
  150. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
  151. package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
  152. package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
  153. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
  154. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
  155. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
  156. package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
  157. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
  158. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
  159. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
  160. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
  161. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
  163. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
  164. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
  165. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
  166. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
  167. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
  168. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
  169. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
  170. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
  171. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
  172. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
  173. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
  174. package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
  175. package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
  176. package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
  177. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
  178. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
  179. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
  180. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
  181. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
  182. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
  183. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
  184. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
  185. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
  186. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
  187. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
  188. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  189. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
  190. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
  191. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
  192. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
  194. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
  195. package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
  196. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
  197. package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
  198. package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
  199. package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
  200. package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
  201. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
  202. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
  203. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
  204. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
  205. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
  206. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
  207. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  208. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
  209. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
  210. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
  211. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
  212. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
  213. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
  214. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
  215. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
  216. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
  217. package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
  218. package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
  219. package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
  220. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
  221. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
  222. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
  223. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
  224. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
  225. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
  226. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
  227. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  228. package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
  229. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
  230. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
  231. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
  232. package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
  233. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
  234. package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
  235. package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
  236. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
  237. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
  238. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
  239. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
  240. package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
  241. package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
  242. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
  243. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
  244. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
  245. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
  246. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
  247. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
  248. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
  249. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
  250. package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
  251. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
  252. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
  253. package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
  254. package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
  255. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
  256. package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
  257. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
  258. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
  259. package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
  260. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
  261. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
  262. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
  263. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
  264. package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
  265. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
  266. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
  267. package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
  268. package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
  269. package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
  270. package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
  271. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
  272. package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
  273. package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
  274. package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
  275. package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
  276. package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
  277. package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
  278. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
  279. package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
  280. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
  281. package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
  282. package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
  283. package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
  284. package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
  285. package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
  286. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
  287. package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
  288. package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
  289. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
  290. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
  291. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
  292. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
  293. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
  294. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
  295. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
  296. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
  297. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
  298. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
  299. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
  300. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
  301. package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
  302. package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
  303. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
  304. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
  305. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
  306. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
  307. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
  308. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
  309. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
  310. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
  311. package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
  312. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
  313. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
  314. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
  315. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
  316. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
  317. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
  318. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  319. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
  320. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
  321. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
  322. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
  323. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
  324. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
  325. package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
  326. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
  327. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
  328. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
  329. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
  330. package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
  331. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
  332. package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
  333. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
  334. package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
  335. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
  336. package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
  337. package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
  338. package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
  339. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
  340. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
  341. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
  342. package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
  343. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
  344. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
  345. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
  346. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
  347. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
  348. package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
  349. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
  350. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
  351. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
  352. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
  353. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
  354. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
  355. package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
  356. package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
  357. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
  358. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
  359. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
  360. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
  361. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
  362. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
  363. package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
  364. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
  365. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
  366. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
  367. package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
  368. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
  369. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
  370. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
  371. package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
  372. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
  373. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
  374. package/dist/extensions/pi-web-agent/src/types.ts +1 -1
  375. package/dist/extensions/pi-zentui/README.md +17 -0
  376. package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
  377. package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
  378. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
  379. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
  380. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
  381. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
  382. package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
  383. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
  384. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
  385. package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
  386. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
  387. package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
  388. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
  389. package/dist/extensions/pi-zentui/package-lock.json +277 -517
  390. package/dist/extensions/pi-zentui/package.json +7 -4
  391. package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
  392. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
  393. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
  394. package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
  395. package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
  396. package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
  397. package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
  398. package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
  399. package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
  400. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
  401. package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
  402. package/dist/index.d.ts +1 -1
  403. package/dist/main.js +4 -1
  404. package/dist/modes/interactive/chat-viewport.d.ts +19 -0
  405. package/dist/modes/interactive/chat-viewport.js +27 -0
  406. package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
  407. package/dist/modes/interactive/components/assistant-message.js +30 -13
  408. package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
  409. package/dist/modes/interactive/components/custom-editor.js +39 -1
  410. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  411. package/dist/modes/interactive/components/index.d.ts +1 -1
  412. package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
  413. package/dist/modes/interactive/components/markdown-transform.js +18 -0
  414. package/dist/modes/interactive/components/model-selector.js +19 -17
  415. package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
  416. package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
  417. package/dist/modes/interactive/components/settings-selector.js +128 -20
  418. package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
  419. package/dist/modes/interactive/components/settings-submenu.js +164 -0
  420. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  421. package/dist/modes/interactive/components/status-indicator.js +10 -3
  422. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  423. package/dist/modes/interactive/components/thinking-selector.js +75 -18
  424. package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
  425. package/dist/modes/interactive/components/tool-execution.js +35 -33
  426. package/dist/modes/interactive/components/trust-selector.js +2 -2
  427. package/dist/modes/interactive/interactive-mode.d.ts +13 -15
  428. package/dist/modes/interactive/interactive-mode.js +144 -80
  429. package/dist/modes/interactive/session-share.d.ts +15 -0
  430. package/dist/modes/interactive/session-share.js +183 -0
  431. package/dist/modes/interactive/theme/dark.json +2 -1
  432. package/dist/modes/interactive/theme/light.json +2 -1
  433. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  434. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  435. package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
  436. package/dist/modes/interactive/theme/theme-json.js +129 -0
  437. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  438. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  439. package/dist/modes/interactive/theme/theme.js +18 -119
  440. package/dist/modes/interactive/tui-renderer.d.ts +20 -0
  441. package/dist/modes/interactive/tui-renderer.js +65 -0
  442. package/dist/utils/exif-orientation.js +2 -3
  443. package/dist/utils/management-http.d.ts +24 -0
  444. package/dist/utils/management-http.js +53 -0
  445. package/dist/utils/text.d.ts +7 -0
  446. package/dist/utils/text.js +8 -0
  447. package/dist/utils/tools-manager.d.ts +1 -0
  448. package/dist/utils/tools-manager.js +43 -21
  449. package/docs/docs.json +0 -4
  450. package/package.json +4 -4
  451. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
  452. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
  453. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
  454. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  455. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
  456. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
  457. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
  458. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
  459. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
  460. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
  461. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
  462. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
  463. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
  464. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
  465. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
  466. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
  467. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
  468. package/dist/skills/implanger/SKILL.md +0 -70
  469. package/dist/skills/output/SKILL.md +0 -49
  470. package/dist/skills/planger/SKILL.md +0 -167
  471. package/dist/skills/workflow/SKILL.md +0 -95
  472. package/docs/workflows.md +0 -18
  473. /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
@@ -1,9 +1,28 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import { spawnSync } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
3
  import * as fs from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import * as path from "node:path";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import type { Message } from "@earendil-works/pi-ai";
8
+ import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
9
+
10
+ // Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
11
+ function ensureProxyAwareHttpDispatcher(): void {
12
+ try {
13
+ // SAFETY: require loads the pinned direct dependency described by these types.
14
+ const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
15
+ const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
16
+ // Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
17
+ EventEmitter.prototype.on.call(dispatcher, "error", () => {});
18
+ undici.setGlobalDispatcher(dispatcher);
19
+ undici.install();
20
+ } catch (error) {
21
+ console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
22
+ }
23
+ }
24
+ const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
25
+ if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
7
26
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
27
  import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
9
28
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
@@ -11,9 +30,9 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
11
30
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
12
31
  import { updateActiveRunIndex } from "./active-run-index.ts";
13
32
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
14
- import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest, type StopRequest } from "./control-channel.ts";
33
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
15
34
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
16
- import { SELESAI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
35
+ import { SELESAI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
17
36
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
18
37
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
19
38
  import {
@@ -28,11 +47,10 @@ import {
28
47
  type AsyncStatus,
29
48
  type ChainOutputMap,
30
49
  type CostSummary,
31
- type LaunchResolvedChildExtensionsV1,
32
- type RuntimeAcknowledgedChildExtensionsV1,
50
+ type LaunchResolvedChildExtensions,
51
+ type RuntimeAcknowledgedChildExtensions,
33
52
  type ModelAttempt,
34
- type PiWriterProcessInstanceExitV1,
35
- type ProcessTreeTerminalV1,
53
+ type PiWriterProcessInstanceExit,
36
54
  type NestedRouteInfo,
37
55
  type NestedRunSummary,
38
56
  type ResolvedControlConfig,
@@ -52,7 +70,6 @@ import {
52
70
  type MaxOutputConfig,
53
71
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
54
72
  truncateOutput,
55
- getSubagentDepthEnv,
56
73
  } from "../../shared/types.ts";
57
74
  import {
58
75
  DEFAULT_CONTROL_CONFIG,
@@ -75,31 +92,29 @@ import {
75
92
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
76
93
  Semaphore,
77
94
  } from "../shared/parallel-utils.ts";
78
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
95
+ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
96
+ import type { InheritedChildRuntime } from "../shared/child-launch.ts";
97
+ import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
98
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
99
+ import type { ChildSessionFactory } from "../shared/child-session.ts";
100
+ import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
101
+ import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
102
+ import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
103
+ import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
104
+ import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
79
105
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
106
  import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
81
- import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
82
107
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
83
- import { clearStructuredOutputCaptures, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
84
- import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
85
- import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
108
+ import { clearStructuredOutputCaptures, createStructuredOutputFileCapture, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
109
+ import { formatMidToolExitError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
110
+ import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
86
111
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
87
112
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
88
113
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
89
114
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
90
115
  import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
91
- import {
92
- SUBAGENT_STARTUP_RETRY_DELAYS_MS,
93
- formatSubagentExtensionConflictError,
94
- formatSubagentStartupRetryExhaustedError,
95
- formatSubagentStartupRetryNote,
96
- isRetryableSubagentStartupFailure,
97
- waitForSubagentStartupRetry,
98
- } from "../shared/subagent-startup-retry.ts";
99
- import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
100
- import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
116
+ import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
101
117
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
102
- import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
103
118
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
104
119
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
105
120
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
@@ -120,6 +135,9 @@ import type { TokenUsage } from "../../shared/types.ts";
120
135
  import {
121
136
  cleanupWorktrees,
122
137
  createWorktrees,
138
+ withWorktreeTransaction,
139
+ WorktreeSetupError,
140
+ type WorktreeSetupProgress,
123
141
  diffWorktrees,
124
142
  findWorktreeTaskCwdConflict,
125
143
  formatWorktreeDiffSummary,
@@ -127,42 +145,42 @@ import {
127
145
  WORKTREE_AGENT_CWD_PLACEHOLDER,
128
146
  type WorktreeSetup,
129
147
  } from "../shared/worktree.ts";
130
- import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
131
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
148
+ import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
149
+ import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
132
150
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
133
151
  import { writeInitialProgressFile } from "../../shared/settings.ts";
134
152
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
135
153
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
136
- import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
154
+ import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
137
155
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
138
- import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
139
156
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
140
157
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
141
158
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
142
159
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
143
160
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
144
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
161
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writeWorktreeSetupHandoff } from "../shared/parallel-handoff.ts";
145
162
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
146
- import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
147
163
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
148
164
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
149
165
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
150
166
  import { runExternalJob } from "../shared/external-job-runner.ts";
151
167
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
152
- import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
168
+ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
153
169
  import {
154
- CHILD_WATCHDOG_CONFIG_ENV,
155
170
  acceptChildWatchdogEvent,
156
171
  applyChildWatchdogMessage,
157
- childWatchdogIsActive,
158
- decodeChildWatchdogConfig,
159
172
  isChildWatchdogStatusEvent,
160
173
  resolveChildWatchdogConfig,
161
- type ChildWatchdogStateSnapshot,
174
+ type ChildWatchdogStatusEvent,
162
175
  } from "../../watchdog/child-status.ts";
163
176
 
164
177
  const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
165
178
 
179
+ // This process hosts child sessions. An ambient copy of pi-subagents loaded
180
+ // into one of them must register nothing; the variable marks the process as a
181
+ // child host.
182
+ process.env[SUBAGENT_CHILD_ENV] = "1";
183
+
166
184
  interface SubagentRunConfig {
167
185
  id: string;
168
186
  steps: RunnerStep[];
@@ -180,10 +198,14 @@ interface SubagentRunConfig {
180
198
  sessionId?: string | null;
181
199
  completionOwnerId?: string;
182
200
  piPackageRoot?: string;
183
- piArgv1?: string;
201
+ /** Test seam: module the runner imports its `ChildSessionFactory` from. */
202
+ childSessionFactoryModule?: string;
203
+ /** The launching executor's own child runtime when it was itself an in-process child. */
204
+ inheritedChildRuntime?: InheritedChildRuntime;
184
205
  worktreeSetupHook?: string;
185
206
  worktreeSetupHookTimeoutMs?: number;
186
207
  worktreeBaseDir?: string;
208
+ baseRef?: string;
187
209
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
188
210
  worktreeBranchPrefix?: string;
189
211
  controlConfig?: ResolvedControlConfig;
@@ -208,8 +230,8 @@ interface SubagentRunConfig {
208
230
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
209
231
  runFanoutBudget?: RunFanoutBudgetDescriptor;
210
232
  launchContractDigest?: string;
211
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
212
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
233
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
234
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
213
235
  runnerProcessInstanceId?: string;
214
236
  launchBarrierToken?: string;
215
237
  parentWorkflowRunId?: string;
@@ -224,12 +246,11 @@ interface StepResult {
224
246
  context?: "fresh" | "fork";
225
247
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
226
248
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
227
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
228
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
249
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
250
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
229
251
  output: string;
230
252
  outputState?: SubagentOutputState;
231
253
  error?: string;
232
- protocolError?: ProtocolOutputLimit;
233
254
  success?: boolean;
234
255
  exitCode: number | null;
235
256
  usage?: Usage;
@@ -268,8 +289,6 @@ interface StepResult {
268
289
  structuredOutputSchemaPath?: string;
269
290
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
270
291
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
271
- writerProcesses?: PiWriterProcessInstanceExitV1[];
272
- writerAttemptCount?: number;
273
292
  runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
274
293
  externalProcess?: ExternalProcessStatus;
275
294
  externalJob?: ExternalJobStatus;
@@ -370,10 +389,6 @@ function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?
370
389
  state.diagnosticsTruncated = true;
371
390
  }
372
391
 
373
- function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
374
- return event.type !== "message_update";
375
- }
376
-
377
392
  function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
378
393
  if (!args || typeof args !== "object" || Array.isArray(args)) return false;
379
394
  if (toolName === "contact_supervisor") {
@@ -456,15 +471,6 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
456
471
  }
457
472
  }
458
473
 
459
- function assistantStartsToolCall(message: Message): boolean {
460
- return Array.isArray(message.content)
461
- && message.content.some((part) => (part as { type?: string }).type === "toolCall");
462
- }
463
-
464
- function isTerminalAssistantStop(message: Message): boolean {
465
- return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
466
- }
467
-
468
474
  type UndefinedOmitted<T extends object> = {
469
475
  [K in keyof T]: Exclude<T[K], undefined>;
470
476
  };
@@ -507,73 +513,6 @@ function resetStepLiveDetail(step: RunnerStatusStep): void {
507
513
  step.recentOutput = [];
508
514
  }
509
515
 
510
- interface ChildEventContext {
511
- eventsPath: string;
512
- runId: string;
513
- stepIndex: number;
514
- agent: string;
515
- }
516
-
517
- interface ChildUsage {
518
- input?: number;
519
- inputTokens?: number;
520
- output?: number;
521
- outputTokens?: number;
522
- cacheRead?: number;
523
- cacheReadTokens?: number;
524
- cacheWrite?: number;
525
- cost?: { total?: number };
526
- }
527
-
528
- type ChildMessage = Message & {
529
- model?: string;
530
- errorMessage?: string;
531
- usage?: ChildUsage;
532
- };
533
-
534
- interface ChildEvent {
535
- type?: string;
536
- message?: ChildMessage;
537
- toolName?: string;
538
- args?: Record<string, unknown>;
539
- willRetry?: unknown;
540
- }
541
-
542
- interface RunPiStreamingResult {
543
- stderr: string;
544
- exitCode: number | null;
545
- messages: Message[];
546
- usage: Usage;
547
- toolCount: number;
548
- durationMs: number;
549
- model?: string;
550
- error?: string;
551
- protocolError?: ProtocolOutputLimit;
552
- finalOutput: string;
553
- outputState: SubagentOutputState;
554
- interrupted?: boolean;
555
- timedOut?: boolean;
556
- stopped?: boolean;
557
- toolBudget?: ToolBudgetState;
558
- toolBudgetBlocked?: boolean;
559
- observedMutationAttempt?: boolean;
560
- structuredOutputToolInvoked?: boolean;
561
- structuredOutputMessageStartIndex?: number;
562
- structuredOutput?: unknown;
563
- watchdog?: ChildWatchdogStateSnapshot;
564
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
565
- processInstanceId: string;
566
- processCloseObservedAt?: number;
567
- processSignal?: string | null;
568
- processTree: ProcessTreeTerminalV1;
569
- currentTool?: string;
570
- currentToolArgs?: string;
571
- currentPath?: string;
572
- afterCompactionSettlement?: boolean;
573
- abortRecoveryDiagnostic?: string;
574
- effects?: import("../../shared/types.ts").EffectsProjection;
575
- }
576
-
577
516
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
578
517
 
579
518
  function formatRequiredOutputError(requiredOutput: {
@@ -608,568 +547,6 @@ function formatChildFailureDiagnostic(input: {
608
547
  return `${baseError.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
609
548
  }
610
549
 
611
- function runPiStreaming(
612
- args: string[],
613
- cwd: string,
614
- outputFile: string,
615
- env?: Record<string, string | undefined>,
616
- piPackageRoot?: string,
617
- piArgv1?: string,
618
- maxSubagentDepth?: number,
619
- childEventContext?: ChildEventContext,
620
- registerInterrupt?: (interrupt: (() => void) | undefined) => void,
621
- onChildEvent?: (event: ChildEvent) => void,
622
- transcriptWriter?: ChildTranscriptWriter,
623
- registerTimeout?: (interrupt: (() => void) | undefined) => void,
624
- timeoutMessage?: string,
625
- registerStop?: (stop: (() => void) | undefined) => void,
626
- stopMessage?: string,
627
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
628
- toolTimeoutMs?: number,
629
- runDeadlineAt?: number,
630
- orcaProgressTab?: OrcaProgressTab,
631
- expectedModelForVerification?: string,
632
- modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>,
633
- mutationTools?: readonly string[],
634
- ): Promise<RunPiStreamingResult> {
635
- return new Promise((resolve) => {
636
- const startedAt = Date.now();
637
- const processInstanceId = randomUUID();
638
- onWriterProcess?.({ state: "spawning" });
639
- const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
640
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
641
- const spawnSpec = getPiSpawnCommand(args, {
642
- ...(piPackageRoot ? { piPackageRoot } : {}),
643
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
644
- });
645
- const child = spawn(spawnSpec.command, spawnSpec.args, {
646
- cwd,
647
- stdio: ["ignore", "pipe", "pipe"],
648
- env: spawnEnv,
649
- windowsHide: true,
650
- detached: process.platform !== "win32",
651
- });
652
- let processTreeController: OwnedProcessTreeController | undefined;
653
- const stderrTail = createBoundedByteTail();
654
- const rawStdoutTail = createBoundedByteTail();
655
- const messages: Message[] = [];
656
- const usage = emptyUsage();
657
- let model: string | undefined;
658
- let writerRegistrationError: string | undefined;
659
- if (typeof child.pid === "number") {
660
- processTreeController = createOwnedProcessTreeController(child.pid);
661
- try {
662
- onWriterProcess?.({ state: "running", pid: child.pid });
663
- } catch (writerError) {
664
- writerRegistrationError = `Failed to record revived Pi writer ownership: ${writerError instanceof Error ? writerError.message : String(writerError)}`;
665
- trySignalChild(child, "SIGKILL");
666
- }
667
- }
668
- let error: string | undefined = writerRegistrationError;
669
- let assistantError: string | undefined;
670
- let interrupted = false;
671
- let timedOut = false;
672
- let stopped = false;
673
- let observedMutationAttempt = false;
674
- let structuredOutputToolInvoked = false;
675
- let structuredOutputMessageStartIndex: number | undefined;
676
- let currentTool: string | undefined;
677
- let currentToolArgs: string | undefined;
678
- let currentPath: string | undefined;
679
- let toolCount = 0;
680
- type ActiveToolCall = { key: string; tool: string; args?: string; path?: string };
681
- let activeToolSequence = 0;
682
- const activeToolCalls = new Map<string, ActiveToolCall>();
683
- const activeToolKeysByName = new Map<string, string[]>();
684
- const refreshCurrentTool = (): void => {
685
- const active = [...activeToolCalls.values()].at(-1);
686
- currentTool = active?.tool;
687
- currentToolArgs = active?.args;
688
- currentPath = active?.path;
689
- };
690
- const recordActiveToolCall = (event: { toolCallId?: unknown; toolName: string; args?: Record<string, unknown> }): void => {
691
- const key = toolTimeoutCallKey(event, ++activeToolSequence);
692
- const active = omitUndefinedProperties({
693
- key,
694
- tool: event.toolName,
695
- args: extractToolArgsPreview(event.args ?? {}),
696
- path: resolveCurrentPath(event.toolName, event.args),
697
- });
698
- activeToolCalls.set(key, active);
699
- const keys = activeToolKeysByName.get(active.tool) ?? [];
700
- keys.push(key);
701
- activeToolKeysByName.set(active.tool, keys);
702
- refreshCurrentTool();
703
- };
704
- const removeActiveToolCall = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
705
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
706
- ? `id:${event.toolCallId}`
707
- : typeof event.toolName === "string"
708
- ? activeToolKeysByName.get(event.toolName)?.[0]
709
- : activeToolCalls.size === 1
710
- ? [...activeToolCalls.keys()][0]
711
- : undefined;
712
- if (!key) return;
713
- const active = activeToolCalls.get(key);
714
- if (!active) return;
715
- activeToolCalls.delete(key);
716
- const keys = activeToolKeysByName.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
717
- if (keys.length > 0) activeToolKeysByName.set(active.tool, keys);
718
- else activeToolKeysByName.delete(active.tool);
719
- refreshCurrentTool();
720
- };
721
- const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
722
- let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
723
- const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
724
- let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
725
- const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
726
- childWatchdogState = snapshot;
727
- };
728
-
729
- const writeOutputLine = (line: string) => {
730
- if (!line.trim()) return;
731
- outputStream.write(`${line}\n`);
732
- orcaProgressTab?.append(`${line}\n`);
733
- };
734
-
735
- const writeOutputText = (text: string) => {
736
- for (const line of text.split("\n")) {
737
- writeOutputLine(line);
738
- }
739
- };
740
-
741
- const appendChildEvent = (event: Record<string, unknown>) => {
742
- if (!childEventContext) return;
743
- if (!shouldPersistChildEvent(event)) return;
744
- appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
745
- ...event,
746
- subagentSource: "child",
747
- subagentRunId: childEventContext.runId,
748
- subagentStepIndex: childEventContext.stepIndex,
749
- subagentAgent: childEventContext.agent,
750
- observedAt: Date.now(),
751
- }), typeof event.type === "string" ? event.type : undefined);
752
- };
753
-
754
- const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
755
- appendChildEvent({ type, line });
756
- if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
757
- else transcriptWriter?.writeStderrLine(line);
758
- };
759
-
760
- const processStdoutLine = (line: string) => {
761
- if (!line.trim()) return;
762
- let event: ChildEvent;
763
- try {
764
- event = JSON.parse(line) as ChildEvent;
765
- } catch {
766
- rawStdoutTail.push(`${line}\n`);
767
- writeOutputLine(line);
768
- appendChildLine("subagent.child.stdout", line);
769
- return;
770
- }
771
-
772
- appendChildEvent(event as unknown as Record<string, unknown>);
773
- transcriptWriter?.writeChildEvent(event);
774
- if (event.type === "compaction_start") compactionStartedReceived = true;
775
- if (event.type === "compaction_end" && event.willRetry === true) {
776
- compactionStartedReceived = false;
777
- afterCompactionSettlement = false;
778
- }
779
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
780
- compactionStartedReceived = false;
781
- afterCompactionSettlement = false;
782
- }
783
- const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
784
- if (event.type === "agent_settled" && lifecycleAction === "start-drain") {
785
- agentSettledReceived = true;
786
- afterCompactionSettlement = compactionStartedReceived;
787
- }
788
- applyChildLifecycle(lifecycleAction);
789
-
790
- if (isChildWatchdogStatusEvent(event)) {
791
- if (!childWatchdogConfig) return;
792
- const next = acceptChildWatchdogEvent({
793
- current: childWatchdogState,
794
- event,
795
- ...(childEventContext ? {
796
- runId: childEventContext.runId,
797
- agent: childEventContext.agent,
798
- childIndex: childEventContext.stepIndex,
799
- } : {}),
800
- });
801
- if (!next) return;
802
- updateChildWatchdogState(next);
803
- onChildEvent?.(event);
804
- if (childWatchdogIsActive(next)) {
805
- if (finalDrainTimer) {
806
- clearTimeout(finalDrainTimer);
807
- finalDrainTimer = undefined;
808
- }
809
- if (finalHardKillTimer) {
810
- clearTimeout(finalHardKillTimer);
811
- finalHardKillTimer = undefined;
812
- }
813
- armWatchdogTail();
814
- } else {
815
- clearWatchdogTailTimer();
816
- if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
817
- }
818
- return;
819
- }
820
-
821
- onChildEvent?.(event);
822
-
823
- if (event.type === "tool_execution_end") {
824
- clearActiveToolTimeout(event);
825
- removeActiveToolCall(event);
826
- return;
827
- }
828
-
829
- if (event.type === "tool_execution_start" && event.toolName) {
830
- toolCount += 1;
831
- armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
832
- recordActiveToolCall({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName, args: event.args });
833
- if (event.toolName === "structured_output") {
834
- structuredOutputToolInvoked = true;
835
- structuredOutputMessageStartIndex = messages.length;
836
- }
837
- observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args, mutationTools);
838
- const toolArgs = extractToolArgsPreview(event.args ?? {});
839
- writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
840
- return;
841
- }
842
-
843
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
844
- if (event.type === "tool_result_end") {
845
- clearActiveToolTimeout(event);
846
- removeActiveToolCall({
847
- toolCallId: (event.message as { toolCallId?: unknown }).toolCallId ?? (event as { toolCallId?: unknown }).toolCallId,
848
- toolName: (event.message as { toolName?: unknown }).toolName ?? event.toolName,
849
- });
850
- }
851
- messages.push(event.message);
852
- const text = extractTextFromContent(event.message.content);
853
- if (text) writeOutputText(text);
854
-
855
- if (childWatchdogConfig && event.type === "message_end") {
856
- const next = applyChildWatchdogMessage(childWatchdogState, event.message);
857
- if (next) updateChildWatchdogState(next);
858
- }
859
- if (event.type !== "message_end" || event.message.role !== "assistant") return;
860
- const hasToolCall = assistantStartsToolCall(event.message);
861
- if (event.message.model) {
862
- model = event.message.model;
863
- if (expectedModelForVerification && !hasToolCall) {
864
- const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, event.message.model, modelVerificationRegistry);
865
- if (modelVerificationError && !error) error = modelVerificationError;
866
- }
867
- }
868
- if (event.message.errorMessage) assistantError = event.message.errorMessage;
869
- const eventUsage = event.message.usage;
870
- if (eventUsage) {
871
- usage.turns++;
872
- usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
873
- usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
874
- usage.cacheRead += eventUsage.cacheRead ?? 0;
875
- usage.cacheWrite += eventUsage.cacheWrite ?? 0;
876
- usage.cost += eventUsage.cost?.total ?? 0;
877
- }
878
- if (isTerminalAssistantStop(event.message)) {
879
- if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
880
- cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
881
- clearAllToolTimeouts();
882
- activeToolCalls.clear();
883
- activeToolKeysByName.clear();
884
- refreshCurrentTool();
885
- applyChildLifecycle(projectChildLifecycle(event, true, childLifecycleState));
886
- }
887
- }
888
- };
889
-
890
- // Guard both cases that can leave the parent waiting on `close` forever:
891
- // a lingering stdio holder after `exit`, or a child that never exits.
892
- const FINAL_STOP_GRACE_MS = 1000;
893
- const HARD_KILL_MS = 3000;
894
- let childExited = false;
895
- let forcedTerminationSignal = false;
896
- let cleanTerminalAssistantStopReceived = false;
897
- let agentSettledReceived = false;
898
- let compactionStartedReceived = false;
899
- let afterCompactionSettlement = false;
900
- let finalDrainTimer: NodeJS.Timeout | undefined;
901
- let finalHardKillTimer: NodeJS.Timeout | undefined;
902
- let watchdogTailTimer: NodeJS.Timeout | undefined;
903
- let protocolHardKillTimer: NodeJS.Timeout | undefined;
904
- let protocolError: ProtocolOutputLimit | undefined;
905
- let settled = false;
906
- applyChildLifecycle = (action: ChildLifecycleAction): void => {
907
- if (action === "cancel-drain") {
908
- if (finalDrainTimer) {
909
- clearTimeout(finalDrainTimer);
910
- finalDrainTimer = undefined;
911
- }
912
- if (finalHardKillTimer) {
913
- clearTimeout(finalHardKillTimer);
914
- finalHardKillTimer = undefined;
915
- }
916
- clearWatchdogTailTimer();
917
- return;
918
- }
919
- if (action === "start-drain") startFinalDrain();
920
- };
921
- const failProtocol = (limit: ProtocolOutputLimit): void => {
922
- if (protocolError) return;
923
- protocolError = limit;
924
- error = formatProtocolOutputLimit(limit);
925
- if (!childExited) {
926
- trySignalChild(child, "SIGTERM");
927
- protocolHardKillTimer = setTimeout(() => {
928
- if (!settled) trySignalChild(child, "SIGKILL");
929
- }, 3000);
930
- protocolHardKillTimer.unref?.();
931
- }
932
- };
933
- const stdoutReader = createBoundedLineReader({
934
- oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
935
- onLine: processStdoutLine,
936
- onLimit: failProtocol,
937
- });
938
- const stderrReader = createBoundedLineReader({
939
- stream: "stderr",
940
- maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
941
- onLine: (line) => appendChildLine("subagent.child.stderr", line),
942
- onLimit: (limit) => appendChildLine("subagent.child.stderr", formatProtocolOutputLimit(limit)),
943
- });
944
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
945
- child.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
946
- child.stderr.on("data", (chunk: Buffer) => {
947
- stderrTail.push(chunk);
948
- stderrReader.push(chunk);
949
- outputStream.write(chunk);
950
- orcaProgressTab?.append(chunk.toString("utf-8"));
951
- });
952
- registerInterrupt?.(() => {
953
- if (settled || timedOut || stopped) return;
954
- interrupted = true;
955
- if (!error) error = "Interrupted. Waiting for explicit next action.";
956
- trySignalChild(child, "SIGINT");
957
- setTimeout(() => {
958
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
959
- }, 1000).unref?.();
960
- });
961
- const terminateForTimeout = (message: string): void => {
962
- if (settled || timedOut || stopped) return;
963
- timedOut = true;
964
- // runPiStreaming's terminal result derives the timeout error from this
965
- // message, so retain the tool-specific reason through finalization.
966
- timeoutMessage = message;
967
- interrupted = false;
968
- error = message;
969
- if (processTreeController) void processTreeController.terminate();
970
- else trySignalChild(child, "SIGTERM");
971
- };
972
- let toolTimeoutSequence = 0;
973
- const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
974
- const activeToolTimeoutKeysByName = new Map<string, string[]>();
975
- const removeToolTimeoutKey = (key: string): void => {
976
- const active = activeToolTimeouts.get(key);
977
- if (!active) return;
978
- clearTimeout(active.timer);
979
- activeToolTimeouts.delete(key);
980
- const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
981
- if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
982
- else activeToolTimeoutKeysByName.delete(active.toolName);
983
- };
984
- const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
985
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
986
- ? `id:${event.toolCallId}`
987
- : typeof event.toolName === "string"
988
- ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
989
- : activeToolTimeouts.size === 1
990
- ? [...activeToolTimeouts.keys()][0]
991
- : undefined;
992
- if (key) removeToolTimeoutKey(key);
993
- };
994
- const clearAllToolTimeouts = (): void => {
995
- for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
996
- };
997
- const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
998
- const timeoutForTool = effectiveToolTimeoutMs(event.toolName, toolTimeoutMs);
999
- if (timeoutForTool === undefined) return;
1000
- const runRemaining = runDeadlineAt === undefined ? undefined : Math.max(0, runDeadlineAt - Date.now());
1001
- if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
1002
- const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
1003
- const toolName = event.toolName;
1004
- const timer = setTimeout(() => {
1005
- removeToolTimeoutKey(key);
1006
- terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
1007
- }, timeoutForTool);
1008
- timer.unref?.();
1009
- activeToolTimeouts.set(key, { toolName, timer });
1010
- const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
1011
- keys.push(key);
1012
- activeToolTimeoutKeysByName.set(toolName, keys);
1013
- };
1014
- registerTimeout?.(() => terminateForTimeout(timeoutMessage ?? "Subagent timed out."));
1015
- registerStop?.(() => {
1016
- if (settled || timedOut || stopped) return;
1017
- stopped = true;
1018
- interrupted = false;
1019
- error = stopMessage ?? "Subagent stopped by user.";
1020
- if (processTreeController) void processTreeController.terminate();
1021
- else trySignalChild(child, "SIGTERM");
1022
- });
1023
- const clearDrainTimers = () => {
1024
- clearAllToolTimeouts();
1025
- if (finalDrainTimer) {
1026
- clearTimeout(finalDrainTimer);
1027
- finalDrainTimer = undefined;
1028
- }
1029
- if (finalHardKillTimer) {
1030
- clearTimeout(finalHardKillTimer);
1031
- finalHardKillTimer = undefined;
1032
- }
1033
- clearWatchdogTailTimer();
1034
- if (protocolHardKillTimer) {
1035
- clearTimeout(protocolHardKillTimer);
1036
- protocolHardKillTimer = undefined;
1037
- }
1038
- };
1039
- function startFinalDrain(): void {
1040
- if (childWatchdogIsActive(childWatchdogState)) {
1041
- armWatchdogTail();
1042
- return;
1043
- }
1044
- if (childExited || finalDrainTimer || settled) return;
1045
- finalDrainTimer = setTimeout(() => {
1046
- if (settled) return;
1047
- const termSent = trySignalChild(child, "SIGTERM");
1048
- if (!termSent) return;
1049
- forcedTerminationSignal = true;
1050
- if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
1051
- error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
1052
- }
1053
- finalHardKillTimer = setTimeout(() => {
1054
- if (settled) return;
1055
- forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
1056
- }, HARD_KILL_MS);
1057
- finalHardKillTimer.unref?.();
1058
- }, FINAL_STOP_GRACE_MS);
1059
- finalDrainTimer.unref?.();
1060
- }
1061
- function clearWatchdogTailTimer(): void {
1062
- if (watchdogTailTimer) {
1063
- clearTimeout(watchdogTailTimer);
1064
- watchdogTailTimer = undefined;
1065
- }
1066
- }
1067
- function armWatchdogTail(): void {
1068
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled) return;
1069
- watchdogTailTimer = setTimeout(() => {
1070
- watchdogTailTimer = undefined;
1071
- updateChildWatchdogState({
1072
- phase: "stale",
1073
- seq: (childWatchdogState?.seq ?? 0) + 1,
1074
- lastUpdate: Date.now(),
1075
- reason: "child watchdog tail timeout",
1076
- timedOut: true,
1077
- });
1078
- startFinalDrain();
1079
- }, childWatchdogConfig?.watchdogTailTimeoutMs ?? 120_000);
1080
- watchdogTailTimer.unref?.();
1081
- }
1082
- child.on("exit", () => {
1083
- childExited = true;
1084
- clearDrainTimers();
1085
- });
1086
- child.on("close", async (exitCode, signal) => {
1087
- settled = true;
1088
- const processCloseObservedAt = Date.now();
1089
- const processTree = processTreeController
1090
- ? await processTreeController.finishAfterWriterClose()
1091
- : { state: "unknown" as const, reason: "verification-failed" as const, diagnostic: "Writer PID was unavailable." };
1092
- try {
1093
- onWriterProcess?.({ state: "none" });
1094
- } catch {
1095
- // The runner still owns and releases the lease during finalization.
1096
- }
1097
- registerInterrupt?.(undefined);
1098
- registerTimeout?.(undefined);
1099
- registerStop?.(undefined);
1100
- clearDrainTimers();
1101
- clearStdioGuard();
1102
- stdoutReader.end();
1103
- stderrReader.end();
1104
- outputStream.end();
1105
- const stderr = stderrTail.text();
1106
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1107
- const finalError = error ?? assistantError;
1108
- const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
1109
- const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
1110
- const forcedDrainError = forcedDrainAfterEmptyTerminal && stderr.trim()
1111
- ? stderr.trim()
1112
- : undefined;
1113
- const signalError = isUnexplainedProcessSignal({
1114
- processSignal: signal,
1115
- interrupted,
1116
- timedOut,
1117
- stopped,
1118
- forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1119
- }) ? formatProcessSignalError(signal!) : undefined;
1120
- resolve(omitUndefinedProperties({
1121
- stderr,
1122
- exitCode: timedOut || stopped ? 1 : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
1123
- messages,
1124
- usage,
1125
- toolCount,
1126
- durationMs: Date.now() - startedAt,
1127
- model,
1128
- error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError ?? forcedDrainError ?? signalError,
1129
- protocolError,
1130
- finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
1131
- outputState: finalOutput.trim() ? "present" : "absent",
1132
- interrupted,
1133
- timedOut,
1134
- stopped,
1135
- observedMutationAttempt,
1136
- structuredOutputToolInvoked,
1137
- structuredOutputMessageStartIndex,
1138
- watchdog: childWatchdogState,
1139
- processInstanceId,
1140
- processCloseObservedAt,
1141
- processSignal: signal,
1142
- processTree,
1143
- currentTool,
1144
- currentToolArgs,
1145
- currentPath,
1146
- afterCompactionSettlement: afterCompactionSettlement || undefined,
1147
- }));
1148
- });
1149
-
1150
- child.on("error", (spawnError) => {
1151
- settled = true;
1152
- try {
1153
- onWriterProcess?.({ state: "none" });
1154
- } catch {
1155
- // The runner still owns and releases the lease during finalization.
1156
- }
1157
- registerInterrupt?.(undefined);
1158
- registerTimeout?.(undefined);
1159
- registerStop?.(undefined);
1160
- clearDrainTimers();
1161
- clearStdioGuard();
1162
- stdoutReader.end();
1163
- stderrReader.end();
1164
- outputStream.end();
1165
- const stderr = stderrTail.text();
1166
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1167
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
1168
- resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId, processTree: { state: "unknown", reason: "verification-failed", diagnostic: spawnErrorMessage } }));
1169
- });
1170
- });
1171
- }
1172
-
1173
550
  function resolvePiPackageRootFallback(): string {
1174
551
  const root = resolveInstalledPiPackageRoot();
1175
552
  if (root) return root;
@@ -1291,15 +668,17 @@ interface SingleStepContext {
1291
668
  flatIndex: number;
1292
669
  flatStepCount: number;
1293
670
  outputFile: string;
1294
- steerInboxDir?: string;
1295
- steerCapabilityPath?: string;
1296
- steerAckDir?: string;
1297
671
  transcriptPath?: string;
1298
672
  piPackageRoot?: string;
1299
- piArgv1?: string;
673
+ /** Factory the runner creates this step's child session through. */
674
+ childSessions: ChildSessionFactory;
675
+ /** The launching executor's own child runtime; nested route, depth, and ceilings come from here. */
676
+ inheritedChildRuntime?: InheritedChildRuntime;
1300
677
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1301
678
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1302
679
  registerStop?: (stop: (() => void) | undefined) => void;
680
+ /** Receives the live child's steer handler while its session runs. */
681
+ registerSteer?: (steer: StepSteerHandler | undefined) => void;
1303
682
  timeoutSignal?: AbortSignal;
1304
683
  stopSignal?: AbortSignal;
1305
684
  timeoutMessage?: string;
@@ -1315,18 +694,20 @@ interface SingleStepContext {
1315
694
  runFanoutBudget?: RunFanoutBudgetDescriptor;
1316
695
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
1317
696
  onChildEvent?: (event: ChildEvent) => void;
1318
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1319
697
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1320
698
  onExternalJob?: (status: ExternalJobStatus) => void;
1321
699
  skipAcceptance?: () => boolean;
1322
- usageBudgetExhausted?: () => boolean;
700
+ /** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
701
+ usageBudgetExhausted?: () => boolean | undefined;
702
+ /** Existing run-owned budget configuration; cost allowance is not settled by the live token ledger. */
703
+ usageBudget?: UsageBudgetConfig;
1323
704
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
1324
705
  trackedMutationEvidenceForCompletionGuard?: boolean;
1325
706
  orcaProgressTab?: OrcaProgressTab;
1326
707
  }
1327
708
 
1328
709
  /** Run a single pi agent step, returning output and metadata */
1329
- async function runSingleStepInner(
710
+ export async function runSingleStepInner(
1330
711
  step: SubagentStep,
1331
712
  ctx: SingleStepContext,
1332
713
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -1421,13 +802,11 @@ async function runSingleStepInner(
1421
802
  model: step.model,
1422
803
  modelCandidates: step.modelCandidates,
1423
804
  mcpDirectTools: step.mcpDirectTools,
1424
- mcpConfig: step.mcpConfig,
1425
- runtimeServerNames: step.runtimeServerNames,
1426
805
  cwd: step.cwd ?? ctx.cwd,
1427
806
  requireReadTool: Boolean(step.skills?.length),
1428
807
  structuredOutput: Boolean(effectiveStructuredOutput),
1429
808
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1430
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
809
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1431
810
  permissionRules: step.permissionRules,
1432
811
  }));
1433
812
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
@@ -1456,8 +835,8 @@ async function runSingleStepInner(
1456
835
  // Derive from the pre-acceptance task so internal acceptance/recovery
1457
836
  // instructions never leak into the display name.
1458
837
  const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
1459
- if (step.effectiveAcceptance) {
1460
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
838
+ if (step.effectiveAcceptance && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
839
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
1461
840
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
1462
841
  }
1463
842
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1634,12 +1013,10 @@ async function runSingleStepInner(
1634
1013
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1635
1014
  let launchResolvedExtensions = step.launchResolvedExtensions;
1636
1015
  const modelAttempts: ModelAttempt[] = [];
1637
- const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1638
- let writerAttemptCount = 0;
1639
1016
  const attemptNotes: string[] = [];
1640
1017
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1641
1018
  let finalRequiredOutputMissing: boolean | undefined;
1642
- let finalResult: RunPiStreamingResult | undefined;
1019
+ let finalResult: RunChildSessionResult | undefined;
1643
1020
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1644
1021
  let structuredAcceptanceReport: unknown;
1645
1022
  let structuredAcceptanceReportError: string | undefined;
@@ -1651,24 +1028,44 @@ async function runSingleStepInner(
1651
1028
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1652
1029
 
1653
1030
  let modelIndex = 0;
1654
- let startupAttemptIndex = 0;
1655
- // Escalated to "file" after an unexplained zero-activity startup failure so
1656
- // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1657
- let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1658
1031
  let contextOverflow = false;
1659
1032
  let launchWarningsEmitted = false;
1660
- let abortRecoveryAttempted = false;
1033
+ let recoveryState: LogicalRecoveryState = "unused";
1034
+ let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
1035
+ const continuationBudget = () => {
1036
+ if (step.toolBudget) return "tool-budget-configured" as const;
1037
+ // Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
1038
+ const exhausted = ctx.usageBudgetExhausted?.();
1039
+ if (exhausted === true) return "exhausted" as const;
1040
+ if (!ctx.usageBudget) return "unconfigured" as const;
1041
+ if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
1042
+ return "available" as const;
1043
+ };
1044
+ const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
1045
+ && !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
1046
+ const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
1047
+ const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
1048
+ const message = error instanceof Error ? error.message : String(error);
1049
+ modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
1050
+ if (candidate) attemptedModels.push(candidate);
1051
+ if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
1052
+ };
1661
1053
  let nextAttemptTask = task;
1662
1054
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1663
1055
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1664
- const recoveringAbort = abortRecoveryAttempted;
1056
+ if (readonlyContinuation && !canContinue()) break;
1057
+ const recoveringAbort = recoveryState === "abort-recovery";
1665
1058
  const attemptTask = nextAttemptTask;
1666
1059
  const candidate = candidates[modelIndex];
1667
1060
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1668
1061
  try {
1669
- assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.agent, runId: ctx.id });
1062
+ assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1670
1063
  } catch (error) {
1671
1064
  const message = error instanceof Error ? error.message : String(error);
1065
+ if (readonlyContinuation) {
1066
+ failContinuationLaunch(candidate, error);
1067
+ break modelAttemptsLoop;
1068
+ }
1672
1069
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1673
1070
  }
1674
1071
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1693,64 +1090,28 @@ async function runSingleStepInner(
1693
1090
  childIndex: ctx.flatIndex,
1694
1091
  })
1695
1092
  : undefined;
1696
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1697
- parentSessionId: step.parentSessionId,
1698
- forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1699
- baseArgs: ["--mode", "json", "-p"],
1700
- task: attemptTask,
1701
- taskDelivery: taskDeliveryOverride,
1702
- sessionEnabled,
1703
- sessionDir,
1704
- sessionFile: step.sessionFile,
1705
- model: candidate,
1706
- inheritProjectContext: step.inheritProjectContext,
1707
- inheritGlobalContext: step.inheritGlobalContext,
1708
- inheritSkills: step.inheritSkills,
1709
- requireReadTool: Boolean(step.skills?.length),
1710
- tools: step.tools,
1711
- excludeTools: step.excludeTools,
1712
- allowNestedSubagents: step.allowNestedSubagents,
1713
- extensions: step.extensions,
1714
- subagentOnlyExtensions: step.subagentOnlyExtensions,
1715
- fast: step.fast,
1716
- modelCandidates: step.modelCandidates,
1717
- systemPrompt: step.systemPrompt ?? "",
1718
- systemPromptMode: step.systemPromptMode,
1719
- mcpDirectTools: step.mcpDirectTools,
1720
- mcpConfig: step.mcpConfig,
1721
- runtimeServerNames: step.runtimeServerNames,
1722
- capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1723
- cwd: step.cwd ?? ctx.cwd,
1724
- promptFileStem: step.agent,
1725
- intercomSessionName: ctx.childIntercomTarget,
1726
- sessionName: childSessionName,
1727
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1728
- runId: ctx.id,
1729
- childAgentName: step.agent,
1730
- childIndex: ctx.flatIndex,
1731
- parentEventSink: ctx.nestedRoute?.eventSink,
1732
- parentControlInbox: ctx.nestedRoute?.controlInbox,
1733
- parentRootRunId: ctx.nestedRoute?.rootRunId,
1734
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
1735
- runFanoutBudget: ctx.runFanoutBudget ? {
1736
- ...ctx.runFanoutBudget,
1737
- ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1738
- } : undefined,
1739
- steerInboxDir: ctx.steerInboxDir,
1740
- steerCapabilityPath: ctx.steerCapabilityPath,
1741
- steerAckDir: ctx.steerAckDir,
1742
- structuredOutput: effectiveStructuredOutput,
1743
- toolBudget: step.toolBudget,
1744
- permissionRules: step.permissionRules,
1745
- permissionAuditPath: step.permissionRules && ctx.artifactsDir
1746
- ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1747
- : undefined,
1748
- childWatchdog,
1749
- waitToolEnabled: step.waitToolEnabled,
1750
- waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1751
- thinkingCeiling: step.thinkingCeiling,
1752
- extensionBindings,
1753
- }));
1093
+ let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1094
+ let launch: ReturnType<typeof buildRunnerChildLaunch>;
1095
+ try {
1096
+ launch = buildRunnerChildLaunch(step, ctx, {
1097
+ sessionEnabled,
1098
+ sessionDir,
1099
+ model: candidate,
1100
+ sessionName: childSessionName,
1101
+ structuredOutput: effectiveStructuredOutput,
1102
+ childWatchdog,
1103
+ watchdogStatus: (event) => watchdogSink?.(event),
1104
+ });
1105
+ } catch (error) {
1106
+ if (!readonlyContinuation) throw error;
1107
+ failContinuationLaunch(candidate, error);
1108
+ break modelAttemptsLoop;
1109
+ }
1110
+ if (effectiveStructuredOutput && launch.config.structuredOutput) {
1111
+ // The runner reads the value back from the runtime's files after the run.
1112
+ launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
1113
+ }
1114
+ const { warnings, capabilityAudit: attemptCapabilityAudit } = launch;
1754
1115
  if (!launchWarningsEmitted && warnings.length > 0) {
1755
1116
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1756
1117
  launchWarningsEmitted = true;
@@ -1766,13 +1127,11 @@ async function runSingleStepInner(
1766
1127
  model: step.model,
1767
1128
  modelCandidates: step.modelCandidates,
1768
1129
  mcpDirectTools: step.mcpDirectTools,
1769
- mcpConfig: step.mcpConfig,
1770
- runtimeServerNames: step.runtimeServerNames,
1771
1130
  cwd: step.cwd ?? ctx.cwd,
1772
1131
  requireReadTool: Boolean(step.skills?.length),
1773
1132
  structuredOutput: Boolean(effectiveStructuredOutput),
1774
1133
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1775
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1134
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1776
1135
  permissionRules: step.permissionRules,
1777
1136
  }));
1778
1137
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
@@ -1801,59 +1160,53 @@ async function runSingleStepInner(
1801
1160
  }));
1802
1161
  }
1803
1162
  capabilityAudit = attemptCapabilityAudit;
1804
- writerAttemptCount += 1;
1805
- const run = await runPiStreaming(
1806
- args,
1807
- step.cwd ?? ctx.cwd,
1808
- ctx.outputFile,
1809
- env,
1810
- ctx.piPackageRoot,
1811
- ctx.piArgv1,
1812
- step.maxSubagentDepth,
1813
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1814
- ctx.registerInterrupt,
1815
- ctx.onChildEvent,
1163
+ // Each attempt rewrites the step output log; synchronous appends keep a
1164
+ // retried attempt from interleaving with the previous attempt's flush.
1165
+ fs.writeFileSync(ctx.outputFile, "", "utf-8");
1166
+ const run = await runChildSession(omitUndefinedProperties({
1167
+ factory: ctx.childSessions,
1168
+ launch,
1169
+ collectReadonlyEvidence: true,
1170
+ readonlyContinuation,
1171
+ canContinue,
1172
+ prompt: `Task: ${attemptTask}`,
1173
+ childWatchdog,
1174
+ childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1175
+ appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
1176
+ writeOutputLine: (line) => {
1177
+ try {
1178
+ fs.appendFileSync(ctx.outputFile, `${line}\n`, "utf-8");
1179
+ } catch {
1180
+ // The output log is observability only.
1181
+ }
1182
+ ctx.orcaProgressTab?.append(`${line}\n`);
1183
+ },
1184
+ registerInterrupt: ctx.registerInterrupt,
1185
+ registerTimeout: ctx.registerTimeout,
1186
+ registerStop: ctx.registerStop,
1187
+ registerSteer: ctx.registerSteer,
1188
+ registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1189
+ timeoutMessage: ctx.timeoutMessage,
1190
+ stopMessage: ctx.stopMessage,
1191
+ onChildEvent: ctx.onChildEvent,
1816
1192
  transcriptWriter,
1817
- ctx.registerTimeout,
1818
- ctx.timeoutMessage,
1819
- ctx.registerStop,
1820
- ctx.stopMessage,
1821
- ctx.onWriterProcess,
1822
- ctx.toolTimeoutMs,
1823
- ctx.deadlineAt,
1824
- ctx.orcaProgressTab,
1193
+ toolTimeoutMs: ctx.toolTimeoutMs,
1194
+ runDeadlineAt: ctx.deadlineAt,
1825
1195
  expectedModelForVerification,
1826
- step.modelVerificationRegistry,
1827
- step.mutationTools,
1828
- );
1829
- if (run.processCloseObservedAt !== undefined) {
1830
- writerProcesses.push({
1831
- processInstanceId: run.processInstanceId,
1832
- kind: "pi-writer",
1833
- attempt: writerAttemptCount - 1,
1834
- closeObservedAt: run.processCloseObservedAt,
1835
- exitCode: run.exitCode,
1836
- signal: run.processSignal ?? null,
1837
- processTree: run.processTree,
1838
- });
1839
- }
1840
- const toolAvailabilityError = run.exitCode === 0 && !run.error
1841
- ? readChildToolDiagnosticError(toolDiagnosticPath)
1842
- : undefined;
1843
- const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
1844
- cleanupTempDir(tempDir);
1196
+ modelVerificationRegistry: step.modelVerificationRegistry,
1197
+ modelResponseAliases: step.modelResponseAliases,
1198
+ mutationTools: step.mutationTools,
1199
+ }));
1200
+ const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1201
+ const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1202
+ const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
1845
1203
  const midToolExitError = run.currentTool
1846
1204
  && isOrdinaryToolForMidToolExit(run.currentTool)
1847
1205
  && !run.interrupted
1848
1206
  && !run.timedOut
1849
1207
  && !run.stopped
1850
- && !run.protocolError
1851
1208
  && !toolAvailabilityError
1852
- ? formatMidToolExitError({
1853
- toolName: run.currentTool,
1854
- exitCode: run.exitCode,
1855
- processSignal: run.processSignal,
1856
- })
1209
+ ? formatMidToolExitError({ toolName: run.currentTool })
1857
1210
  : undefined;
1858
1211
 
1859
1212
  let structuredOutput: unknown;
@@ -1896,7 +1249,7 @@ async function runSingleStepInner(
1896
1249
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1897
1250
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1898
1251
  : undefined;
1899
- const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1252
+ const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1900
1253
  const completionToolPlan = resolvedTaskToolPlan;
1901
1254
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1902
1255
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
@@ -1915,14 +1268,27 @@ async function runSingleStepInner(
1915
1268
  }))
1916
1269
  : undefined;
1917
1270
  const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1271
+ let arbitration = { triggered: completionGuard?.triggered === true && !mutationAttemptObserved, rescued: false };
1272
+ if (arbitration.triggered) {
1273
+ const modelContext = launch.capture.completionIntentContext?.();
1274
+ arbitration = await arbitrateCompletionGuardRescue({
1275
+ guardTriggered: true,
1276
+ task: taskForCompletionGuard,
1277
+ // Construct lazily too: the shared gate refuses overlength tasks before
1278
+ // registry/auth/model work. The child has already shut down normally.
1279
+ arbiter: modelContext ? async (task) => createTaskMutationArbiter(modelContext)?.(task) ?? "unavailable" : undefined,
1280
+ });
1281
+ }
1918
1282
  const completionEvidence = planCompletionEvidence({
1919
1283
  guard: completionGuard,
1284
+ guardTriggered: arbitration.triggered,
1285
+ arbiterRescued: arbitration.rescued,
1920
1286
  completionGuardEnabled,
1921
1287
  mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1922
1288
  implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1923
1289
  mutationAttemptObserved,
1924
1290
  mutationEvidence: completionMutationEvidence,
1925
- agentContractV1: isAgentContractV1(step.agentContract),
1291
+ agentContractEnabled: isAgentContract(step.agentContract),
1926
1292
  });
1927
1293
  const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1928
1294
  const requiredOutput = step.outputMode === "file-only" && step.outputPath
@@ -1940,19 +1306,10 @@ async function runSingleStepInner(
1940
1306
  : run.error && run.exitCode === 0
1941
1307
  ? 1
1942
1308
  : run.exitCode;
1943
- const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
1944
- processSignal: run.processSignal,
1945
- interrupted: run.interrupted,
1946
- timedOut: run.timedOut,
1947
- stopped: run.stopped,
1948
- })) ? formatProcessSignalError(run.processSignal!) : undefined;
1949
1309
  const underlyingError = toolAvailabilityError
1950
1310
  ?? midToolExitError
1951
1311
  ?? structuredError
1952
1312
  ?? run.error
1953
- ?? signalError
1954
- ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1955
- ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1956
1313
  ?? emptyOutputError
1957
1314
  ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1958
1315
  ?? (hiddenError?.hasError
@@ -1960,13 +1317,7 @@ async function runSingleStepInner(
1960
1317
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1961
1318
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1962
1319
  : undefined);
1963
- const error = formatSubagentExtensionConflictError(
1964
- underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1965
- {
1966
- agent: step.agent,
1967
- ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1968
- },
1969
- );
1320
+ const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1970
1321
  const attempt: ModelAttempt = omitUndefinedProperties({
1971
1322
  model: candidate ?? run.model ?? step.model ?? "default",
1972
1323
  success: effectiveExitCode === 0 && !error,
@@ -1975,7 +1326,7 @@ async function runSingleStepInner(
1975
1326
  usage: run.usage,
1976
1327
  });
1977
1328
  modelAttempts.push(attempt);
1978
- if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1329
+ if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1979
1330
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1980
1331
  finalOutputSnapshot = outputSnapshot;
1981
1332
  if (step.toolBudget) {
@@ -1992,13 +1343,12 @@ async function runSingleStepInner(
1992
1343
  afterCompactionSettlement: run.afterCompactionSettlement === true,
1993
1344
  });
1994
1345
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1995
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunPiStreamingResult;
1346
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
1996
1347
  const abortRecovery = !attempt.success ? planAbortRecovery({
1997
1348
  messages: run.messages,
1998
1349
  error,
1999
- processSignal: run.processSignal,
2000
1350
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
2001
- alreadyResumed: abortRecoveryAttempted,
1351
+ alreadyResumed: recoveryState !== "unused",
2002
1352
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
2003
1353
  interrupted: run.interrupted,
2004
1354
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -2018,59 +1368,59 @@ async function runSingleStepInner(
2018
1368
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
2019
1369
  if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
2020
1370
  if (attempt.success) break modelAttemptsLoop;
1371
+ // The shared token is consumed before sibling creation; no third dispatch of any kind.
1372
+ if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
2021
1373
  if (recoveringAbort) break modelAttemptsLoop;
2022
1374
  if (abortRecovery?.action === "resume") {
2023
- abortRecoveryAttempted = true;
1375
+ recoveryState = "abort-recovery";
2024
1376
  nextAttemptTask = abortRecovery.prompt;
2025
1377
  attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
2026
1378
  continue;
2027
1379
  }
2028
1380
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
2029
1381
 
2030
- const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
2031
- exitCode: effectiveExitCode,
2032
- error,
2033
- finalOutput: run.finalOutput,
2034
- messageCount: run.messages.length,
2035
- toolCount: run.toolCount,
2036
- usage: run.usage,
2037
- durationMs: run.durationMs,
2038
- protocolError: run.protocolError,
2039
- processSignal: run.processSignal,
2040
- observedMutationAttempt: run.observedMutationAttempt,
2041
- interrupted: run.interrupted,
2042
- timedOut: run.timedOut,
2043
- stopped: run.stopped,
2044
- }));
2045
- const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2046
- if (startupFailure && retryDelayMs !== undefined) {
2047
- const retryNote = formatSubagentStartupRetryNote({
2048
- model: attempt.model,
2049
- attempt: startupAttemptIndex + 1,
2050
- maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
2051
- delayMs: retryDelayMs,
1382
+ const source = getSettledReadonlyChild(run);
1383
+ if (source) {
1384
+ const captured = launch.capture.completionIntentContext?.();
1385
+ const sourceModel = captured?.model;
1386
+ const retained = getReadonlySessionEvidence(source)!;
1387
+ // Modality/capacity assessment, not another history/provenance validator.
1388
+ // Images and unknown content have no certified token bound in this host slice.
1389
+ const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
1390
+ || (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
1391
+ const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
1392
+ const resolvedCandidates = candidates.map((reference, index) => {
1393
+ // Only exact registry identities qualify; aliases/default guesses remain ineligible.
1394
+ const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
1395
+ const slash = base.indexOf("/");
1396
+ const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
1397
+ // Reserve the source window for runtime/system/tool overhead, plus a conservative
1398
+ // byte bound for the complete retained text and new prompt; never assume a fit
1399
+ // merely because a 429 request was sent. Equal/smaller windows remain denied.
1400
+ const compatible = textOnly && model && sourceModel && model.input?.includes("text")
1401
+ && Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
1402
+ && model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
1403
+ && model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
1404
+ return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
1405
+ tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
2052
1406
  });
2053
- const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
2054
- if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
2055
- if (!taskDeliveryOverride && run.processSignal === "SIGKILL") {
2056
- taskDeliveryOverride = "file";
2057
- attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
1407
+ const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
1408
+ lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
1409
+ effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
1410
+ && !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
1411
+ && !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
1412
+ budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
1413
+ if (plan.kind === "continue" && canContinue()) {
1414
+ recoveryState = plan.recoveryState;
1415
+ const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
1416
+ readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
1417
+ nextAttemptTask = plan.prompt;
1418
+ modelIndex = plan.candidateIndex;
1419
+ attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
1420
+ continue;
2058
1421
  }
2059
- attempt.error = retryNote;
2060
- attemptNotes.push(retryNote);
2061
- startupAttemptIndex += 1;
2062
- continue;
2063
- }
2064
- if (startupFailure) {
2065
- const startupError = formatSubagentStartupRetryExhaustedError({
2066
- model: attempt.model,
2067
- attempts: startupAttemptIndex + 1,
2068
- });
2069
- attempt.error = startupError;
2070
- finalResult.error = startupError;
2071
- finalResult.finalOutput = startupError;
2072
- break modelAttemptsLoop;
2073
1422
  }
1423
+
2074
1424
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
2075
1425
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
2076
1426
  if (isContextOverflow(error)) {
@@ -2081,7 +1431,6 @@ async function runSingleStepInner(
2081
1431
  if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
2082
1432
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
2083
1433
  modelIndex += 1;
2084
- startupAttemptIndex = 0;
2085
1434
  }
2086
1435
 
2087
1436
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -2109,7 +1458,7 @@ async function runSingleStepInner(
2109
1458
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
2110
1459
  : undefined;
2111
1460
  const outputState: SubagentOutputState = finalResult?.outputState === "present"
2112
- || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1461
+ || (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
2113
1462
  || Boolean(childWrittenOutput?.trim())
2114
1463
  ? "present"
2115
1464
  : resolvedOutput.savedPath
@@ -2153,7 +1502,7 @@ async function runSingleStepInner(
2153
1502
  cwd: step.cwd ?? ctx.cwd,
2154
1503
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
2155
1504
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
2156
- reportOptional: isAgentContractV1(step.agentContract),
1505
+ reportOptional: isAgentContract(step.agentContract),
2157
1506
  artifactsDir: ctx.artifactsDir,
2158
1507
  runId: ctx.id,
2159
1508
  watchdog: finalResult?.watchdog,
@@ -2169,7 +1518,7 @@ async function runSingleStepInner(
2169
1518
  : acceptance
2170
1519
  : undefined;
2171
1520
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
2172
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
1521
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
2173
1522
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2174
1523
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
2175
1524
  const baseFinalError = stoppedAfterAcceptance
@@ -2211,7 +1560,7 @@ async function runSingleStepInner(
2211
1560
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2212
1561
  launchContractDigest: actualLaunchContractDigest,
2213
1562
  launchResolvedExtensions,
2214
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1563
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
2215
1564
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
2216
1565
  transcriptError: transcriptWriter?.getError(),
2217
1566
  skills: step.skills,
@@ -2230,7 +1579,6 @@ async function runSingleStepInner(
2230
1579
  outputState,
2231
1580
  exitCode: effectiveFinalExitCode,
2232
1581
  error: effectiveFinalError,
2233
- protocolError: finalResult?.protocolError,
2234
1582
  sessionFile: step.sessionFile,
2235
1583
  intercomTarget: ctx.childIntercomTarget,
2236
1584
  model: finalResult?.model,
@@ -2241,6 +1589,7 @@ async function runSingleStepInner(
2241
1589
  totalCost: costSummaryFromAttempts(modelAttempts),
2242
1590
  usage,
2243
1591
  artifactPaths,
1592
+ savedOutputPath: finalizedOutput.savedPath,
2244
1593
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
2245
1594
  metadataSaveError: artifactErrors.metadataSaveError,
2246
1595
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
@@ -2248,24 +1597,21 @@ async function runSingleStepInner(
2248
1597
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2249
1598
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2250
1599
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2251
- processSignal: finalResult?.processSignal,
2252
1600
  timeoutRecovery,
2253
1601
  toolBudget,
2254
1602
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2255
1603
  completionGuardTriggered: completionGuardTriggeredFinal,
2256
- ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
2257
- structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1604
+ ...((finalResult as (RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1605
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2258
1606
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2259
1607
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2260
1608
  acceptance: effectiveAcceptance,
2261
1609
  watchdog: finalResult?.watchdog,
2262
1610
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2263
1611
  launchResolvedExtensions,
2264
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2265
- writerProcesses,
2266
- writerAttemptCount,
1612
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
2267
1613
  });
2268
- return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1614
+ return isAgentContract(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
2269
1615
  }
2270
1616
 
2271
1617
  async function runSingleStep(
@@ -2356,12 +1702,15 @@ function markParallelGroupSetupFailure(input: {
2356
1702
  const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
2357
1703
  const task = input.group.parallel[taskIndex];
2358
1704
  if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
2359
- statusStep.status = "failed";
1705
+ const stopped = statusStep.stopped || statusStep.stopRequested || input.statusPayload.stopped;
1706
+ const paused = !stopped && input.statusPayload.state === "paused";
1707
+ statusStep.status = stopped ? "stopped" : paused ? "paused" : "failed";
2360
1708
  statusStep.startedAt = input.failedAt;
2361
1709
  statusStep.endedAt = input.failedAt;
2362
1710
  statusStep.durationMs = 0;
2363
- statusStep.exitCode = 1;
2364
- input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, success: false, exitCode: 1, sessionFile: task.sessionFile }));
1711
+ statusStep.exitCode = paused ? 0 : 1;
1712
+ statusStep.error ??= input.setupError;
1713
+ input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, error: input.setupError, success: false, exitCode: paused ? 0 : 1, sessionFile: task.sessionFile, stopped: stopped || undefined, interrupted: paused || undefined, timedOut: input.statusPayload.timedOut || undefined }));
2365
1714
  }
2366
1715
  input.statusPayload.currentStep = input.groupStartFlatIndex;
2367
1716
  input.statusPayload.lastUpdate = input.failedAt;
@@ -2518,7 +1867,10 @@ async function runSingleStepWithTimeout(
2518
1867
  ctx: SingleStepContext,
2519
1868
  parentDeadlineAt?: number,
2520
1869
  ): Promise<SingleStepResult> {
2521
- if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
1870
+ if (step.timeoutMs === undefined) return runSingleStep(step, parentDeadlineAt === undefined ? ctx : {
1871
+ ...ctx,
1872
+ deadlineAt: ctx.deadlineAt === undefined ? parentDeadlineAt : Math.min(ctx.deadlineAt, parentDeadlineAt),
1873
+ });
2522
1874
 
2523
1875
  const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
2524
1876
  const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
@@ -2555,9 +1907,9 @@ async function runSingleStepWithTimeout(
2555
1907
  }
2556
1908
  }
2557
1909
 
2558
- async function runSubagent(
1910
+ export async function runSubagent(
2559
1911
  config: SubagentRunConfig,
2560
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
1912
+ childSessions: ChildSessionFactory,
2561
1913
  ): Promise<void> {
2562
1914
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
2563
1915
  config;
@@ -2577,9 +1929,11 @@ async function runSubagent(
2577
1929
  const activeChildInterrupts = new Map<number, () => void>();
2578
1930
  const activeChildTimeouts = new Map<number, () => void>();
2579
1931
  const activeChildStops = new Map<number, () => void>();
1932
+ const activeChildSteers = new Map<number, StepSteerHandler>();
1933
+ /** Steers routed to a running step before its session was created. */
1934
+ const queuedStepSteers = new Map<number, SteerRequest[]>();
2580
1935
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2581
1936
  const pendingStepSteers: SteerRequest[] = [];
2582
- const steeringCapabilities = new Map<number, SteerCapability>();
2583
1937
  let interrupted = false;
2584
1938
  let currentActivityState: ActivityState | undefined;
2585
1939
  let activityTimer: NodeJS.Timeout | undefined;
@@ -2591,6 +1945,8 @@ async function runSubagent(
2591
1945
  const stopMessage = "Subagent stopped by user.";
2592
1946
  const timeoutAbortController = new AbortController();
2593
1947
  const stopAbortController = new AbortController();
1948
+ const setupInterruptController = new AbortController();
1949
+ const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
2594
1950
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
2595
1951
  let latestSessionFile: string | undefined;
2596
1952
 
@@ -2757,20 +2113,29 @@ async function runSubagent(
2757
2113
  },
2758
2114
  onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
2759
2115
  });
2760
- const queueActiveRunIndex = (): void => {
2761
- const state = statusPayload.state;
2116
+ const queueActiveRunIndex = (status: AsyncStatus): void => {
2117
+ const state = status.state;
2762
2118
  if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
2763
- indexPersistence.write(asyncDir, { state, toolCallId: statusPayload.toolCallId }, (_filePath, payload) => {
2119
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
2764
2120
  const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
2765
2121
  updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
2766
2122
  });
2767
2123
  };
2124
+ let finalResultCommitted = false;
2125
+ let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
2768
2126
  const runPersistence = createCapacityResilientJsonWriter({
2769
2127
  keepAlive: true,
2770
- onSuccess: (filePath) => {
2771
- if (filePath === statusPath) queueActiveRunIndex();
2128
+ onSuccess: (filePath, payload) => {
2129
+ if (filePath === statusPath) queueActiveRunIndex(payload as AsyncStatus);
2130
+ if (filePath === resultPath && finalResultPublication) {
2131
+ finalResultCommitted = true;
2132
+ finalResultPublication.resolve();
2133
+ }
2134
+ },
2135
+ onError: (error, filePath) => {
2136
+ console.error(`Failed to persist async run state '${filePath}':`, error);
2137
+ if (filePath === resultPath) finalResultPublication?.reject(error);
2772
2138
  },
2773
- onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
2774
2139
  });
2775
2140
  try {
2776
2141
  fs.mkdirSync(asyncDir, { recursive: true });
@@ -2778,7 +2143,7 @@ async function runSubagent(
2778
2143
  if (!isStorageCapacityError(error)) throw error;
2779
2144
  console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
2780
2145
  }
2781
- runPersistence.write(statusPath, statusPayload);
2146
+ runPersistence.write(statusPath, { ...statusPayload });
2782
2147
 
2783
2148
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2784
2149
  const currentUsageTotals = (): CostSummary => {
@@ -2797,6 +2162,13 @@ async function runSubagent(
2797
2162
  setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
2798
2163
  return statusPayload.usageBudget;
2799
2164
  };
2165
+ // Continuation admission only: the existing ledger has no in-flight cost or
2166
+ // external/import usage coverage. Never change ordinary budget enforcement.
2167
+ let continuationUsageUncertain = config.steps.some(isDynamicRunnerGroup) || flatSteps.some((step) => Boolean(step.runner || step.importAsyncRoot));
2168
+ const continuationUsageBudgetExhausted = (): boolean | undefined => {
2169
+ const exhausted = refreshUsageBudget()?.exhausted === true;
2170
+ return exhausted ? true : config.usageBudget && continuationUsageUncertain ? undefined : false;
2171
+ };
2800
2172
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
2801
2173
  if (!config.nestedRoute || !config.nestedSelf) return;
2802
2174
  try {
@@ -2851,7 +2223,6 @@ async function runSubagent(
2851
2223
  for (const node of graph.nodes) updateNode(node);
2852
2224
  statusPayload.workflowGraph = graph;
2853
2225
  };
2854
- let finalResultCommitted = false;
2855
2226
  const statusResultState = (): AsyncStatus["state"] | undefined => {
2856
2227
  if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2857
2228
  return statusPayload.state;
@@ -2913,14 +2284,15 @@ async function runSubagent(
2913
2284
  }), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
2914
2285
  };
2915
2286
  const writeStatusPayloadNow = (): void => {
2287
+ if (finalResultPublication) return;
2916
2288
  refreshWorkflowGraph();
2917
2289
  writeRecoverableStatusResult();
2918
- runPersistence.write(statusPath, statusPayload);
2290
+ runPersistence.write(statusPath, { ...statusPayload });
2919
2291
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2920
2292
  };
2921
2293
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
2922
2294
  const writeStatusPayload = (immediate = true): void => {
2923
- if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
2295
+ if (immediate || statusPayload.state !== "running") {
2924
2296
  if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
2925
2297
  return;
2926
2298
  }
@@ -3037,6 +2409,16 @@ async function runSubagent(
3037
2409
  activeChildStops.set(flatIndex, stop);
3038
2410
  if (stopped || childStopRequests.has(flatIndex)) stop();
3039
2411
  };
2412
+ const registerStepSteer = (flatIndex: number, steer: StepSteerHandler | undefined): void => {
2413
+ if (!steer) {
2414
+ activeChildSteers.delete(flatIndex);
2415
+ return;
2416
+ }
2417
+ activeChildSteers.set(flatIndex, steer);
2418
+ const queued = queuedStepSteers.get(flatIndex);
2419
+ queuedStepSteers.delete(flatIndex);
2420
+ for (const request of queued ?? []) steerLiveChild(flatIndex, request);
2421
+ };
3040
2422
  const interruptActiveChildren = (): void => {
3041
2423
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
3042
2424
  };
@@ -3489,22 +2871,15 @@ async function runSubagent(
3489
2871
  if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
3490
2872
  if (step.status === "pending") return { index, state: "scheduled" as const };
3491
2873
  if (step.status !== "running") return { index, state: "failed" as const, reason: `child is ${step.status}` };
3492
- if (steeringCapabilities.get(index)?.supported === false) return { index, state: "failed" as const, reason: "child Pi session does not support steering" };
3493
2874
  return { index, state: "routed" as const };
3494
2875
  });
3495
2876
  recordSteeringLifecycle(request, targetStates);
3496
2877
  emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
3497
2878
  for (const target of targetStates) {
3498
2879
  if (target.state === "routed") {
3499
- try {
3500
- enqueueStepSteer(asyncDir, target.index, request);
3501
- updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
3502
- emitSteeringEvent("subagent.steer.routed", request, target.index);
3503
- } catch (error) {
3504
- markSteeringAttention(target.index);
3505
- updateSteeringLifecycleTarget(request.id, target.index, "failed", now, { reason: error instanceof Error ? error.message : String(error) });
3506
- emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: error instanceof Error ? error.message : String(error) });
3507
- }
2880
+ updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
2881
+ emitSteeringEvent("subagent.steer.routed", request, target.index);
2882
+ steerLiveChild(target.index, request);
3508
2883
  } else if (target.state === "failed") {
3509
2884
  markSteeringAttention(target.index);
3510
2885
  emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
@@ -3516,27 +2891,42 @@ async function runSubagent(
3516
2891
  statusPayload.lastUpdate = now;
3517
2892
  writeStatusPayload();
3518
2893
  };
3519
- const consumeSteerAck = (ack: SteerAck): void => {
2894
+ /** Record the outcome of handing a routed steer to the live child session. */
2895
+ const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
3520
2896
  const lifecycle = steeringStatus(statusPayload);
3521
- const request = lifecycle.recent.find((candidate) => candidate.id === ack.requestId);
3522
- if (!request || !request.targets.some((target) => target.index === ack.index)) return;
3523
- const late = fs.existsSync(steeringMarkerPath(ack.requestId));
2897
+ const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2898
+ if (!request || !request.targets.some((target) => target.index === index)) return;
2899
+ const late = fs.existsSync(steeringMarkerPath(requestId));
3524
2900
  const now = Date.now();
3525
- if (ack.state === "delivered") {
3526
- updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
3527
- emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, deliveryStatus: "delivered", message: ack.message });
3528
- } else if (ack.state === "queued") {
3529
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "queued", now);
3530
- emitSteeringEvent("subagent.steer.queued", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { deliveryStatus: "queued", message: ack.message });
2901
+ if (delivery.state === "delivered") {
2902
+ updateSteeringLifecycleTarget(requestId, index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
2903
+ emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { late, deliveryStatus: "delivered", message: delivery.message });
2904
+ } else if (delivery.state === "queued") {
2905
+ updateSteeringLifecycleTarget(requestId, index, "queued", now);
2906
+ emitSteeringEvent("subagent.steer.queued", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { deliveryStatus: "queued", message: delivery.message });
3531
2907
  } else {
3532
- markSteeringAttention(ack.index);
3533
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
3534
- emitSteeringEvent("subagent.steer.failed", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { reason: ack.message });
2908
+ markSteeringAttention(index);
2909
+ updateSteeringLifecycleTarget(requestId, index, "failed", now, { reason: delivery.message });
2910
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { reason: delivery.message });
3535
2911
  }
3536
- emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
2912
+ emitTerminalSteeringNotice(requestId, `Steering failed for run ${id}: ${delivery.message}`);
3537
2913
  statusPayload.lastUpdate = now;
3538
2914
  writeStatusPayload();
3539
2915
  };
2916
+ /** Hand a routed steer to the step's live session, or hold it until the session exists. */
2917
+ const steerLiveChild = (index: number, request: SteerRequest): void => {
2918
+ const steer = activeChildSteers.get(index);
2919
+ if (!steer) {
2920
+ const queued = queuedStepSteers.get(index) ?? [];
2921
+ queued.push(request);
2922
+ queuedStepSteers.set(index, queued);
2923
+ return;
2924
+ }
2925
+ void steer(request).then(
2926
+ (delivery) => applySteerDelivery(request.id, index, delivery),
2927
+ (error) => applySteerDelivery(request.id, index, { state: "failed", message: error instanceof Error ? error.message : String(error) }),
2928
+ );
2929
+ };
3540
2930
  const flushPendingStepSteers = (flatIndex: number): void => {
3541
2931
  const remaining: SteerRequest[] = [];
3542
2932
  for (const request of pendingStepSteers.splice(0)) {
@@ -3558,6 +2948,7 @@ async function runSubagent(
3558
2948
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
3559
2949
  const step = statusPayload.steps[flatIndex];
3560
2950
  if (!step) return;
2951
+ const previousActivityState = step.activityState;
3561
2952
  const now = Date.now();
3562
2953
  statusPayload.currentStep = flatIndex;
3563
2954
  if (isChildWatchdogStatusEvent(event)) {
@@ -3620,6 +3011,7 @@ async function runSubagent(
3620
3011
  tokens: step.tokens?.total,
3621
3012
  toolCount: step.toolCount,
3622
3013
  currentTool: step.currentTool,
3014
+ toolCallId: event.toolCallId,
3623
3015
  currentToolDurationMs: 0,
3624
3016
  currentPath: step.currentPath,
3625
3017
  })));
@@ -3691,6 +3083,10 @@ async function runSubagent(
3691
3083
  appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
3692
3084
  step.turnCount = (step.turnCount ?? 0) + 1;
3693
3085
  const usage = event.message.usage;
3086
+ if (config.usageBudget) {
3087
+ const known = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0;
3088
+ if (!known(usage?.input ?? usage?.inputTokens) || !known(usage?.output ?? usage?.outputTokens)) continuationUsageUncertain = true;
3089
+ }
3694
3090
  if (usage) {
3695
3091
  const input = usage.input ?? usage.inputTokens ?? 0;
3696
3092
  const output = usage.output ?? usage.outputTokens ?? 0;
@@ -3710,7 +3106,8 @@ async function runSubagent(
3710
3106
  statusPayload.lastActivityAt = now;
3711
3107
  statusPayload.lastUpdate = now;
3712
3108
  maybeEmitActiveLongRunning(flatIndex, now);
3713
- writeStatusPayload(false);
3109
+ // A sibling may keep aggregate attention unchanged; publish this step's transition.
3110
+ writeStatusPayload(step.activityState !== previousActivityState);
3714
3111
  };
3715
3112
  const updateRunnerActivityState = (now: number): boolean => {
3716
3113
  if (!controlConfig.enabled) return false;
@@ -3782,10 +3179,55 @@ async function runSubagent(
3782
3179
  activityTimer.unref?.();
3783
3180
  }
3784
3181
 
3182
+ const publishSetupUnknown = (progress: WorktreeSetupProgress): void => {
3183
+ if (!progress.unknown) return;
3184
+ const proof = {
3185
+ version: 1 as const, state: "unknown" as const, runId: id,
3186
+ runnerProcessInstanceId: config.runnerProcessInstanceId ?? "unknown",
3187
+ reason: "process-tree-unverified" as const,
3188
+ diagnostic: `Worktree setup settlement unknown: ${progress.unknown}; manual reconciliation required; ${parallelHandoffPath(asyncDir)}`,
3189
+ };
3190
+ statusPayload.processTerminal = proof;
3191
+ // Publish the sticky proof independently of in-process child writer counts.
3192
+ writeAtomicJson(processTerminalPath(asyncDir), proof);
3193
+ };
3194
+ const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3195
+ let admitted = false;
3196
+ try { await withWorktreeTransaction(() => {
3197
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3198
+ admitted = true;
3199
+ action();
3200
+ }); }
3201
+ catch (error) {
3202
+ if (admitted) throw error;
3203
+ const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
3204
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3205
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3206
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3207
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [],
3208
+ cleanup: { state: "partial", pruned: false, errors: [reason], tasks: setup.worktrees.map((worktree) => ({
3209
+ index: worktree.index, path: worktree.path, branch: worktree.branch, provider: worktree.provider, naming: worktree.naming,
3210
+ worktreeRemoved: false, branchRemoved: false, preserved: true, reason,
3211
+ })) },
3212
+ });
3213
+ previousOutput = [previousOutput, reason, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
3214
+ writeStatusPayload();
3215
+ }
3216
+ };
3217
+ const cleanupRemainingWorktree = (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number) => finalizeWorktree(setup, stepIndex, flatStartIndex, () => {
3218
+ const cleanup = cleanupWorktrees(setup);
3219
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3220
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3221
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3222
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [], cleanup,
3223
+ });
3224
+ writeStatusPayload();
3225
+ }, config.deadlineAt);
3785
3226
  const interruptRunner = () => {
3786
3227
  consumeInterruptRequest(asyncDir);
3787
3228
  if (interrupted || statusPayload.state !== "running") return;
3788
3229
  interrupted = true;
3230
+ setupInterruptController.abort();
3789
3231
  const now = Date.now();
3790
3232
  statusPayload.state = "paused";
3791
3233
  currentActivityState = undefined;
@@ -3894,23 +3336,6 @@ async function runSubagent(
3894
3336
  pendingStepSteers.push(request);
3895
3337
  }
3896
3338
  },
3897
- onSteerCapability: (capability) => {
3898
- steeringCapabilities.set(capability.index, capability);
3899
- if (!capability.supported) {
3900
- const now = Date.now();
3901
- const lifecycle = steeringStatus(statusPayload);
3902
- for (const request of lifecycle.recent) {
3903
- if (!request.targets.some((target) => target.index === capability.index && (target.state === "routed" || target.state === "scheduled"))) continue;
3904
- markSteeringAttention(capability.index);
3905
- updateSteeringLifecycleTarget(request.id, capability.index, "failed", now, { reason: "child Pi session does not support steering" });
3906
- emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child Pi session does not support steering" }, capability.index, { reason: "child Pi session does not support steering" });
3907
- emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child ${capability.index} does not support steering.`);
3908
- }
3909
- statusPayload.lastUpdate = now;
3910
- writeStatusPayload();
3911
- }
3912
- },
3913
- onSteerAck: consumeSteerAck,
3914
3339
  });
3915
3340
  if (config.deadlineAt !== undefined) {
3916
3341
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
@@ -3967,7 +3392,7 @@ async function runSubagent(
3967
3392
  : [undefined]
3968
3393
  : model ? [model] : [undefined];
3969
3394
  for (const candidate of candidates) {
3970
- assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.parallel.agent, runId: id });
3395
+ assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3971
3396
  }
3972
3397
  }
3973
3398
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
@@ -4036,14 +3461,14 @@ async function runSubagent(
4036
3461
  cwd,
4037
3462
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
4038
3463
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
4039
- reportOptional: isAgentContractV1(step.agentContract),
3464
+ reportOptional: isAgentContract(step.agentContract),
4040
3465
  }))
4041
3466
  : undefined;
4042
3467
  const groupStopped = stopped || stopAbortController.signal.aborted;
4043
3468
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
4044
3469
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
4045
3470
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
4046
- const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3471
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
4047
3472
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
4048
3473
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
4049
3474
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -4237,11 +3662,9 @@ async function runSubagent(
4237
3662
  artifactsDir, artifactConfig, id,
4238
3663
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4239
3664
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4240
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4241
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4242
- steerAckDir: steerAcksDir(asyncDir, fi),
4243
3665
  piPackageRoot: config.piPackageRoot,
4244
- piArgv1: config.piArgv1,
3666
+ childSessions,
3667
+ inheritedChildRuntime: config.inheritedChildRuntime,
4245
3668
  childIntercomTarget: config.childIntercomTargets?.[fi],
4246
3669
  orchestratorIntercomTarget: config.controlIntercomTarget,
4247
3670
  nestedRoute: config.nestedRoute,
@@ -4250,6 +3673,7 @@ async function runSubagent(
4250
3673
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4251
3674
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4252
3675
  registerStop: (stop) => registerStepStop(fi, stop),
3676
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4253
3677
  timeoutSignal: timeoutAbortController.signal,
4254
3678
  stopSignal: stopAbortController.signal,
4255
3679
  trackedMutationEvidenceForCompletionGuard: false,
@@ -4258,11 +3682,11 @@ async function runSubagent(
4258
3682
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4259
3683
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4260
3684
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4261
- onWriterProcess,
4262
3685
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4263
3686
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4264
3687
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4265
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
3688
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
3689
+ usageBudget: config.usageBudget,
4266
3690
  orcaProgressTab,
4267
3691
  }), config.deadlineAt);
4268
3692
  const taskEndTime = Date.now();
@@ -4339,7 +3763,6 @@ async function runSubagent(
4339
3763
  output: pr.output,
4340
3764
  outputState: pr.outputState,
4341
3765
  error: pr.error,
4342
- protocolError: pr.protocolError,
4343
3766
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4344
3767
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4345
3768
  skipped: pr.skipped,
@@ -4379,7 +3802,7 @@ async function runSubagent(
4379
3802
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
4380
3803
  const acceptanceFailures = parallelResults
4381
3804
  .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
4382
- .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3805
+ .filter(({ result, task }) => isAgentContract(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4383
3806
  if (acceptanceFailures.length > 0) {
4384
3807
  const message = acceptanceFailures
4385
3808
  .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
@@ -4409,13 +3832,13 @@ async function runSubagent(
4409
3832
  cwd,
4410
3833
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
4411
3834
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
4412
- reportOptional: isAgentContractV1(step.agentContract),
3835
+ reportOptional: isAgentContract(step.agentContract),
4413
3836
  }))
4414
3837
  : undefined;
4415
3838
  const groupStopped = stopped || stopAbortController.signal.aborted;
4416
3839
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
4417
3840
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
4418
- const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3841
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
4419
3842
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
4420
3843
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
4421
3844
  if (groupError) {
@@ -4494,19 +3917,23 @@ async function runSubagent(
4494
3917
  break;
4495
3918
  }
4496
3919
  try {
4497
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3920
+ worktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3921
+ signal: setupSignal,
3922
+ deadlineAt: config.deadlineAt,
4498
3923
  agents: group.parallel.map((task) => task.agent),
4499
3924
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
4500
3925
  tasks: group.parallel.map((task) => task.task),
4501
3926
  provider: config.worktreeProvider,
3927
+ baseRef: config.baseRef,
4502
3928
  branchPrefix: config.worktreeBranchPrefix,
4503
3929
  setupHook: config.worktreeSetupHook
4504
3930
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4505
3931
  : undefined,
4506
3932
  baseDir: config.worktreeBaseDir,
4507
- beforeCreate: (plannedSetup) => {
4508
- for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
4509
- const pendingHandoff = writePendingParallelHandoff({
3933
+ onProgress: (progress) => {
3934
+ publishSetupUnknown(progress);
3935
+ for (const worktree of progress.setup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3936
+ const pendingHandoff = writeWorktreeSetupHandoff({
4510
3937
  manifestPath: parallelHandoffPath(asyncDir),
4511
3938
  runId: id,
4512
3939
  mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
@@ -4514,16 +3941,25 @@ async function runSubagent(
4514
3941
  cwd,
4515
3942
  stepIndex,
4516
3943
  flatStartIndex: groupStartFlatIndex,
4517
- setup: plannedSetup,
3944
+ progress,
4518
3945
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4519
3946
  });
3947
+ if (!pendingHandoff) return;
4520
3948
  statusPayload.parallelHandoff = pendingHandoff;
4521
3949
  statusPayload.lastUpdate = Date.now();
4522
3950
  writeStatusPayload();
4523
3951
  },
4524
3952
  }));
3953
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3954
+ if (setupSignal.aborted) throw new Error(stopped ? stopMessage : timedOut ? timeoutMessage ?? "Subagent timed out." : "Subagent paused during worktree setup.");
4525
3955
  } catch (error) {
3956
+ if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
3957
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4526
3958
  const setupError = error instanceof Error ? error.message : String(error);
3959
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
3960
+ for (let index = groupStartFlatIndex; index < groupStartFlatIndex + group.parallel.length; index++) {
3961
+ if (childStopRequests.has(index)) markChildStopped(index);
3962
+ }
4527
3963
  const failedAt = Date.now();
4528
3964
  markParallelGroupSetupFailure({
4529
3965
  statusPayload,
@@ -4636,11 +4072,9 @@ async function runSubagent(
4636
4072
  artifactsDir, artifactConfig, id,
4637
4073
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4638
4074
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4639
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4640
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4641
- steerAckDir: steerAcksDir(asyncDir, fi),
4642
4075
  piPackageRoot: config.piPackageRoot,
4643
- piArgv1: config.piArgv1,
4076
+ childSessions,
4077
+ inheritedChildRuntime: config.inheritedChildRuntime,
4644
4078
  childIntercomTarget: config.childIntercomTargets?.[fi],
4645
4079
  orchestratorIntercomTarget: config.controlIntercomTarget,
4646
4080
  nestedRoute: config.nestedRoute,
@@ -4649,6 +4083,7 @@ async function runSubagent(
4649
4083
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4650
4084
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4651
4085
  registerStop: (stop) => registerStepStop(fi, stop),
4086
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4652
4087
  timeoutSignal: timeoutAbortController.signal,
4653
4088
  stopSignal: stopAbortController.signal,
4654
4089
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4657,11 +4092,11 @@ async function runSubagent(
4657
4092
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4658
4093
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4659
4094
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4660
- onWriterProcess,
4661
4095
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4662
4096
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4663
4097
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4664
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4098
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4099
+ usageBudget: config.usageBudget,
4665
4100
  orcaProgressTab,
4666
4101
  }), config.deadlineAt);
4667
4102
  if (task.sessionFile) {
@@ -4785,7 +4220,6 @@ async function runSubagent(
4785
4220
  output: pr.output,
4786
4221
  outputState: pr.outputState,
4787
4222
  error: pr.error,
4788
- protocolError: pr.protocolError,
4789
4223
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4790
4224
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4791
4225
  skipped: pr.skipped,
@@ -4840,47 +4274,50 @@ async function runSubagent(
4840
4274
  })),
4841
4275
  );
4842
4276
  if (worktreeSetup) {
4843
- const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
4844
- if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4277
+ const setup = worktreeSetup;
4845
4278
  worktreeFinalized = true;
4846
- const manifestPath = parallelHandoffPath(asyncDir);
4847
- const handoff = {
4848
- manifestPath,
4849
- runId: id,
4850
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4851
- source: "async" as const,
4852
- cwd,
4853
- stepIndex,
4854
- flatStartIndex: groupStartFlatIndex,
4855
- setup: worktreeSetup,
4856
- diffs: captured.diffs,
4857
- results: parallelResults.map((result) => ({
4858
- agent: result.agent,
4859
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4860
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4861
- ...(config.lane ? { lane: config.lane } : {}),
4862
- status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4863
- processSignal: result.processSignal,
4864
- interrupted: result.interrupted,
4865
- timedOut: result.timedOut,
4866
- stopped: result.stopped,
4867
- }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4868
- summary: result.output || result.error || "(no output)",
4869
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4870
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4871
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4872
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4873
- })),
4874
- };
4875
- try {
4876
- writeParallelHandoffGroup(handoff);
4877
- const cleanup = cleanupWorktrees(worktreeSetup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4878
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4879
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4880
- } catch (error) {
4881
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4882
- }
4883
- writeStatusPayload();
4279
+ await finalizeWorktree(setup, stepIndex, groupStartFlatIndex, () => {
4280
+ const captured = captureParallelWorktreeDiffs(setup, asyncDir, stepIndex, group);
4281
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4282
+ const manifestPath = parallelHandoffPath(asyncDir);
4283
+ const handoff = {
4284
+ manifestPath,
4285
+ runId: id,
4286
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4287
+ source: "async" as const,
4288
+ cwd,
4289
+ stepIndex,
4290
+ flatStartIndex: groupStartFlatIndex,
4291
+ setup,
4292
+ diffs: captured.diffs,
4293
+ results: parallelResults.map((result) => ({
4294
+ agent: result.agent,
4295
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4296
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4297
+ ...(config.lane ? { lane: config.lane } : {}),
4298
+ status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4299
+ processSignal: result.processSignal,
4300
+ interrupted: result.interrupted,
4301
+ timedOut: result.timedOut,
4302
+ stopped: result.stopped,
4303
+ }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4304
+ summary: result.output || result.error || "(no output)",
4305
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4306
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4307
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4308
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4309
+ })),
4310
+ };
4311
+ try {
4312
+ writeParallelHandoffGroup(handoff);
4313
+ const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4314
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4315
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4316
+ } catch (error) {
4317
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4318
+ }
4319
+ writeStatusPayload();
4320
+ });
4884
4321
  }
4885
4322
 
4886
4323
  appendJsonl(eventsPath, JSON.stringify({
@@ -4889,12 +4326,12 @@ async function runSubagent(
4889
4326
  runId: id,
4890
4327
  stepIndex,
4891
4328
  success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
4892
- && parallelResults.every((result, index) => !(isAgentContractV1(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
4329
+ && parallelResults.every((result, index) => !(isAgentContract(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
4893
4330
  }));
4894
4331
 
4895
4332
  const acceptanceGateFailure = parallelResults
4896
4333
  .map((result, index) => ({ result, index, task: group.parallel[index] }))
4897
- .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4334
+ .find(({ result, task }) => isAgentContract(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4898
4335
  if (acceptanceGateFailure) {
4899
4336
  statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
4900
4337
  writeStatusPayload();
@@ -4904,7 +4341,7 @@ async function runSubagent(
4904
4341
  break;
4905
4342
  }
4906
4343
  } finally {
4907
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
4344
+ if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
4908
4345
  }
4909
4346
  } else {
4910
4347
  const seqStep = step as SubagentStep;
@@ -4931,22 +4368,26 @@ async function runSubagent(
4931
4368
  let singleWorktreeSetup: WorktreeSetup | undefined;
4932
4369
  if (seqStep.worktree) {
4933
4370
  try {
4934
- singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4371
+ singleWorktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4372
+ signal: setupSignal,
4373
+ deadlineAt: config.deadlineAt,
4935
4374
  agents: [seqStep.agent],
4936
4375
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4937
4376
  tasks: [seqStep.task],
4938
4377
  provider: config.worktreeProvider,
4378
+ baseRef: config.baseRef,
4939
4379
  branchPrefix: config.worktreeBranchPrefix,
4940
4380
  setupHook: config.worktreeSetupHook
4941
4381
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4942
4382
  : undefined,
4943
4383
  baseDir: config.worktreeBaseDir,
4944
- beforeCreate: (plannedSetup) => {
4945
- const worktree = plannedSetup.worktrees[0];
4384
+ onProgress: (progress) => {
4385
+ publishSetupUnknown(progress);
4386
+ const worktree = progress.setup.worktrees[0];
4946
4387
  if (worktree) {
4947
4388
  setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4948
4389
  }
4949
- const pendingHandoff = writePendingParallelHandoff({
4390
+ const pendingHandoff = writeWorktreeSetupHandoff({
4950
4391
  manifestPath: parallelHandoffPath(asyncDir),
4951
4392
  runId: id,
4952
4393
  mode: "single",
@@ -4954,19 +4395,47 @@ async function runSubagent(
4954
4395
  cwd,
4955
4396
  stepIndex,
4956
4397
  flatStartIndex: flatIndex,
4957
- setup: plannedSetup,
4398
+ progress,
4958
4399
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4959
4400
  });
4401
+ if (!pendingHandoff) return;
4960
4402
  statusPayload.parallelHandoff = pendingHandoff;
4961
4403
  statusPayload.lastUpdate = Date.now();
4962
4404
  writeStatusPayload();
4963
4405
  },
4964
4406
  }));
4965
4407
  } catch (error) {
4966
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4967
- throw error;
4408
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
4409
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4410
+ const message = error instanceof Error ? error.message : String(error);
4411
+ if (childStopRequests.has(flatIndex)) markChildStopped(flatIndex);
4412
+ const statusStep = requiredStatusStep(statusPayload, flatIndex);
4413
+ statusStep.status = stopped || childStopRequests.has(flatIndex) ? "stopped" : interrupted ? "paused" : "failed";
4414
+ statusStep.error ??= message;
4415
+ statusStep.exitCode = interrupted ? 0 : 1;
4416
+ statusStep.endedAt = Date.now();
4417
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4418
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4419
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4420
+ : interrupted ? pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4421
+ : { agent: seqStep.agent, output: message, error: message, exitCode: 1, success: false });
4422
+ statusPayload.error ??= message;
4423
+ writeStatusPayload();
4424
+ flatIndex++;
4425
+ break;
4968
4426
  }
4969
4427
  }
4428
+ if (singleWorktreeSetup && config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4429
+ if (singleWorktreeSetup && (timedOut || stopped || interrupted || childStopRequests.has(flatIndex))) {
4430
+ await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4431
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4432
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4433
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4434
+ : pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4435
+ if (interrupted) requiredStatusStep(statusPayload, flatIndex).status = "paused";
4436
+ flatIndex++;
4437
+ continue;
4438
+ }
4970
4439
  const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
4971
4440
  const stepStartTime = Date.now();
4972
4441
  statusPayload.currentStep = flatIndex;
@@ -5003,11 +4472,9 @@ async function runSubagent(
5003
4472
  artifactsDir, artifactConfig, id,
5004
4473
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
5005
4474
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
5006
- steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
5007
- steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
5008
- steerAckDir: steerAcksDir(asyncDir, flatIndex),
5009
4475
  piPackageRoot: config.piPackageRoot,
5010
- piArgv1: config.piArgv1,
4476
+ childSessions,
4477
+ inheritedChildRuntime: config.inheritedChildRuntime,
5011
4478
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
5012
4479
  orchestratorIntercomTarget: config.controlIntercomTarget,
5013
4480
  nestedRoute: config.nestedRoute,
@@ -5016,6 +4483,7 @@ async function runSubagent(
5016
4483
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
5017
4484
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
5018
4485
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4486
+ registerSteer: (steer) => registerStepSteer(flatIndex, steer),
5019
4487
  timeoutSignal: timeoutAbortController.signal,
5020
4488
  stopSignal: stopAbortController.signal,
5021
4489
  timeoutMessage,
@@ -5023,15 +4491,15 @@ async function runSubagent(
5023
4491
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
5024
4492
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
5025
4493
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
5026
- onWriterProcess,
5027
4494
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
5028
4495
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
5029
4496
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
5030
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4497
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4498
+ usageBudget: config.usageBudget,
5031
4499
  orcaProgressTab,
5032
4500
  }), config.deadlineAt);
5033
4501
  } catch (error) {
5034
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4502
+ if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
5035
4503
  throw error;
5036
4504
  }
5037
4505
  if (seqStep.sessionFile) {
@@ -5051,7 +4519,6 @@ async function runSubagent(
5051
4519
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
5052
4520
  outputState: singleResult.outputState,
5053
4521
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
5054
- protocolError: singleResult.protocolError,
5055
4522
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
5056
4523
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
5057
4524
  sessionFile: singleResult.sessionFile,
@@ -5064,6 +4531,7 @@ async function runSubagent(
5064
4531
  totalCost: singleResult.totalCost,
5065
4532
  usage: singleResult.usage,
5066
4533
  artifactPaths: singleResult.artifactPaths,
4534
+ savedOutputPath: singleResult.savedOutputPath,
5067
4535
  transcriptPath: singleResult.transcriptPath,
5068
4536
  transcriptError: singleResult.transcriptError,
5069
4537
  effects: singleResult.effects,
@@ -5184,48 +4652,51 @@ async function runSubagent(
5184
4652
  }));
5185
4653
  if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
5186
4654
  if (singleWorktreeSetup && !singleResult.detached) {
5187
- const diffs = diffWorktrees(singleWorktreeSetup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
5188
- const diffSummary = formatWorktreeDiffSummary(diffs);
5189
- const manifestPath = parallelHandoffPath(asyncDir);
5190
- const handoff = {
5191
- manifestPath,
5192
- runId: id,
5193
- mode: "single" as const,
5194
- source: "async" as const,
5195
- cwd,
5196
- stepIndex,
5197
- flatStartIndex: flatIndex,
5198
- setup: singleWorktreeSetup,
5199
- diffs,
5200
- results: [{
5201
- agent: singleResult.agent,
5202
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
5203
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
5204
- ...(config.lane ? { lane: config.lane } : {}),
5205
- status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
5206
- summary: singleResult.output || singleResult.error || "(no output)",
5207
- ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
5208
- ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
5209
- ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
5210
- ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
5211
- }],
5212
- };
5213
- try {
5214
- writeParallelHandoffGroup(handoff);
5215
- const cleanup = cleanupWorktrees(singleWorktreeSetup, {
5216
- kind: "preserve",
5217
- capturedDiffs: diffs,
5218
- handoffManifestPath: manifestPath,
5219
- ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
5220
- ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
5221
- : {}),
5222
- });
5223
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
5224
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
5225
- } catch (error) {
5226
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
5227
- }
5228
- writeStatusPayload();
4655
+ const setup = singleWorktreeSetup;
4656
+ await finalizeWorktree(setup, stepIndex, flatIndex, () => {
4657
+ const diffs = diffWorktrees(setup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4658
+ const diffSummary = formatWorktreeDiffSummary(diffs);
4659
+ const manifestPath = parallelHandoffPath(asyncDir);
4660
+ const handoff = {
4661
+ manifestPath,
4662
+ runId: id,
4663
+ mode: "single" as const,
4664
+ source: "async" as const,
4665
+ cwd,
4666
+ stepIndex,
4667
+ flatStartIndex: flatIndex,
4668
+ setup,
4669
+ diffs,
4670
+ results: [{
4671
+ agent: singleResult.agent,
4672
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4673
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4674
+ ...(config.lane ? { lane: config.lane } : {}),
4675
+ status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4676
+ summary: singleResult.output || singleResult.error || "(no output)",
4677
+ ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4678
+ ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4679
+ ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4680
+ ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4681
+ }],
4682
+ };
4683
+ try {
4684
+ writeParallelHandoffGroup(handoff);
4685
+ const cleanup = cleanupWorktrees(setup, {
4686
+ kind: "preserve",
4687
+ capturedDiffs: diffs,
4688
+ handoffManifestPath: manifestPath,
4689
+ ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4690
+ ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4691
+ : {}),
4692
+ });
4693
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4694
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4695
+ } catch (error) {
4696
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4697
+ }
4698
+ writeStatusPayload();
4699
+ });
5229
4700
  }
5230
4701
 
5231
4702
  if (singleResult.completionGuardTriggered) {
@@ -5243,7 +4714,7 @@ async function runSubagent(
5243
4714
  }
5244
4715
 
5245
4716
  flatIndex++;
5246
- if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4717
+ if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
5247
4718
  statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
5248
4719
  writeStatusPayload();
5249
4720
  break;
@@ -5319,7 +4790,7 @@ async function runSubagent(
5319
4790
  clearTimeout(timeoutTimer);
5320
4791
  timeoutTimer = undefined;
5321
4792
  }
5322
- if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
4793
+ if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
5323
4794
  timedOut = true;
5324
4795
  }
5325
4796
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
@@ -5329,6 +4800,11 @@ async function runSubagent(
5329
4800
  stopped: result.stopped,
5330
4801
  })));
5331
4802
  const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
4803
+ // Flush while still nonterminal; deferred status retries retain that state snapshot.
4804
+ statusWriteCoalescer.flush(statusPath);
4805
+ const publication = new Promise<void>((resolve, reject) => {
4806
+ finalResultPublication = { resolve, reject };
4807
+ });
5332
4808
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
5333
4809
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
5334
4810
  disposeControlInbox();
@@ -5387,6 +4863,9 @@ async function runSubagent(
5387
4863
  statusPayload.error = `Step failed: ${failedStep.agent}`;
5388
4864
  }
5389
4865
  }
4866
+ let childSessionDisposal: Promise<void> | undefined;
4867
+ const disposeChildSessions = (): Promise<void> => childSessionDisposal ??= childSessions.dispose()
4868
+ .catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
5390
4869
  try {
5391
4870
  runPersistence.write(resultPath, {
5392
4871
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -5409,7 +4888,6 @@ async function runSubagent(
5409
4888
  output: r.output,
5410
4889
  outputState: r.outputState,
5411
4890
  error: r.error,
5412
- protocolError: r.protocolError,
5413
4891
  success: r.success,
5414
4892
  skipped: r.skipped || undefined,
5415
4893
  interrupted: r.interrupted || undefined,
@@ -5428,6 +4906,7 @@ async function runSubagent(
5428
4906
  totalCost: r.totalCost,
5429
4907
  usage: r.usage,
5430
4908
  artifactPaths: r.artifactPaths,
4909
+ savedOutputPath: r.savedOutputPath,
5431
4910
  outputSaveError: r.outputSaveError,
5432
4911
  metadataSaveError: r.metadataSaveError,
5433
4912
  truncated: r.truncated,
@@ -5482,16 +4961,19 @@ async function runSubagent(
5482
4961
  ...(taskIndex !== undefined && { taskIndex }),
5483
4962
  ...(totalTasks !== undefined && { totalTasks }),
5484
4963
  }, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
5485
- finalResultCommitted = true;
4964
+ // Only capacity deferral releases settled sessions before terminal publication.
4965
+ if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
5486
4966
  } catch (err) {
5487
4967
  const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
5488
4968
  console.error(message, err);
5489
4969
  statusPayload.state = "failed";
5490
4970
  statusPayload.error = message;
5491
4971
  statusPayload.lastUpdate = Date.now();
4972
+ } finally {
4973
+ finalResultPublication = undefined;
5492
4974
  }
5493
4975
  writeStatusPayload();
5494
- orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
4976
+ await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
5495
4977
  appendJsonl(
5496
4978
  eventsPath,
5497
4979
  JSON.stringify({
@@ -5526,14 +5008,20 @@ async function runSubagent(
5526
5008
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
5527
5009
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
5528
5010
  }));
5529
- if (runPersistence.pendingCount() === 0) runPersistence.dispose();
5530
- if (indexPersistence.pendingCount() === 0) indexPersistence.dispose();
5011
+ // Preserve normal-success disposal ordering, then drain remaining persistence retries.
5012
+ await disposeChildSessions();
5013
+ while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
5014
+ await new Promise((resolve) => setTimeout(resolve, 200));
5015
+ }
5016
+ runPersistence.dispose();
5017
+ indexPersistence.dispose();
5531
5018
  if (config.runnerProcessInstanceId) {
5532
- const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5019
+ // Children run inside this process, so no step has writer processes to prove terminal.
5020
+ const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
5533
5021
  const expectedWriters: Record<string, number> = {};
5534
- for (const [index, result] of results.entries()) {
5535
- writers[String(index)] = result.writerProcesses ?? [];
5536
- expectedWriters[String(index)] = result.writerAttemptCount ?? 0;
5022
+ for (const index of results.keys()) {
5023
+ writers[String(index)] = [];
5024
+ expectedWriters[String(index)] = 0;
5537
5025
  }
5538
5026
  const candidate: ProcessTerminalCandidate = {
5539
5027
  version: 1,
@@ -5615,7 +5103,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5615
5103
  }
5616
5104
  }
5617
5105
  }
5618
- await runSubagent(config, lease ? (writer) => lease!.updateWriter(writer) : undefined);
5106
+ const childSessions = await loadRunnerChildSessionFactory(config);
5107
+ try {
5108
+ await runSubagent(config, childSessions);
5109
+ } finally {
5110
+ try {
5111
+ await childSessions.dispose();
5112
+ } catch (error) {
5113
+ console.error("Failed to dispose runner child sessions:", error);
5114
+ }
5115
+ }
5619
5116
  } catch (error) {
5620
5117
  if (!startupCommitted) {
5621
5118
  try {
@@ -5644,12 +5141,19 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5644
5141
  }
5645
5142
 
5646
5143
  function startConfiguredSubagent(config: SubagentRunConfig): void {
5647
- runConfiguredSubagent(config).catch((runErr) => {
5648
- console.error("Subagent runner error:", runErr);
5649
- process.exit(1);
5650
- });
5144
+ // Child sessions and the extensions loaded into them may leave handles
5145
+ // behind even after shutdown; the run is fully persisted by now, so exit
5146
+ // explicitly instead of waiting for the event loop to drain.
5147
+ runConfiguredSubagent(config).then(
5148
+ () => process.exit(0),
5149
+ (runErr) => {
5150
+ console.error("Subagent runner error:", runErr);
5151
+ process.exit(1);
5152
+ },
5153
+ );
5651
5154
  }
5652
5155
 
5156
+ if (isRunnerEntrypoint) {
5653
5157
  const configArg = process.argv[2];
5654
5158
  if (configArg) {
5655
5159
  try {
@@ -5680,4 +5184,5 @@ if (configArg) {
5680
5184
  process.exit(1);
5681
5185
  }
5682
5186
  });
5683
- }
5187
+ }
5188
+ }