@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -3,17 +3,20 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@selesai/code";
6
- import { type AgentConfig, type AgentScope } from "../../agents/agents.ts";
6
+ import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts";
8
8
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
9
  import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
10
10
  import { executeChain } from "./chain-execution.ts";
11
+ import { beginForegroundChild, finishForegroundChild, updateForegroundChild } from "./foreground-control.ts";
11
12
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
12
13
  import { handleManagementAction } from "../../agents/agent-management.ts";
13
14
  import { buildDoctorReport } from "../../extension/doctor.ts";
14
15
  import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
15
16
  import { runSync } from "./execution.ts";
16
- import { resolveModelCandidate, resolveSubagentModelOverride } from "../shared/model-fallback.ts";
17
+ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
18
+ import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
19
+ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate } from "../shared/model-fallback.ts";
17
20
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
18
21
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
19
22
  import { recordRun } from "../shared/run-history.ts";
@@ -36,27 +39,34 @@ import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsy
36
39
  import type { ScheduledRunAction } from "../background/scheduled-runs.ts";
37
40
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
38
41
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
39
- import { validateAcceptanceInput } from "../shared/acceptance.ts";
40
- import { createForkContextResolver } from "../../shared/fork-context.ts";
42
+ import { validateExecutionAcceptance } from "../shared/acceptance.ts";
43
+ import { createForkContextResolver, forkedChildRequiresThinkingOff } from "../../shared/fork-context.ts";
41
44
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
42
45
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
43
46
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
44
- import { DEFAULT_TURN_BUDGET_GRACE_TURNS } from "../shared/turn-budget.ts";
47
+ import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
48
+ import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
45
49
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
50
+ import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
51
+ import { isAgentContractV1 } from "../shared/agent-contract.ts";
46
52
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
53
+ import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
47
54
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
48
55
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
56
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
57
+ import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
49
58
  import {
50
59
  attachNestedChildrenToResultChildren,
51
60
  buildSubagentResultIntercomPayload,
52
- deliverSubagentIntercomMessageEvent,
53
61
  deliverSubagentResultIntercomEvent,
54
62
  formatSubagentResultReceipt,
55
63
  resolveSubagentResultStatus,
56
64
  stripDetailsOutputsForIntercomReceipt,
57
65
  } from "../../intercom/result-intercom.ts";
58
- import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
59
- import { deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts";
66
+ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
67
+ import { deliverInterruptRequest, deliverStopRequest, requestAsyncSteer } from "../background/control-channel.ts";
68
+ import { waitForSteeringAction } from "../background/steering.ts";
69
+ import { steerAsyncRun } from "./async-steering-action.ts";
60
70
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
61
71
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
62
72
  import { attachRootChildrenToSteps, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
@@ -75,14 +85,18 @@ import {
75
85
  } from "../shared/worktree.ts";
76
86
  import {
77
87
  type AgentProgress,
88
+ type AsyncStatus,
78
89
  type AcceptanceInput,
90
+ type AgentContract,
79
91
  type ArtifactConfig,
80
92
  type ArtifactPaths,
81
93
  type ControlConfig,
82
94
  type ControlEvent,
83
95
  type Details,
84
96
  type ExtensionConfig,
97
+ type ForegroundRunControl,
85
98
  type IntercomEventBus,
99
+ type JsonSchemaObject,
86
100
  type MaxOutputConfig,
87
101
  type NestedRouteInfo,
88
102
  type NestedRunSummary,
@@ -96,12 +110,13 @@ import {
96
110
  type SubagentState,
97
111
  ASYNC_DIR,
98
112
  DEFAULT_ARTIFACT_CONFIG,
113
+ DEFAULT_FORK_PREAMBLE,
99
114
  RESULTS_DIR,
100
115
  SUBAGENT_ACTIONS,
101
116
  SUBAGENT_CONTROL_EVENT,
102
117
  SUBAGENT_CONTROL_INTERCOM_EVENT,
118
+ SUBAGENT_FOREGROUND_COMPLETE_EVENT,
103
119
  checkSubagentDepth,
104
- resolveMaxSubagentSpawnsPerSession,
105
120
  resolveTopLevelParallelConcurrency,
106
121
  resolveTopLevelParallelMaxTasks,
107
122
  resolveChildMaxSubagentDepth,
@@ -109,8 +124,7 @@ import {
109
124
  wrapForkTask,
110
125
  } from "../../shared/types.ts";
111
126
 
112
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset"]);
113
-
127
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure"]);
114
128
  interface TaskParam {
115
129
  agent: string;
116
130
  task: string;
@@ -122,7 +136,9 @@ interface TaskParam {
122
136
  progress?: boolean;
123
137
  model?: string;
124
138
  skill?: string | string[] | boolean;
139
+ outputSchema?: JsonSchemaObject;
125
140
  acceptance?: AcceptanceInput;
141
+ agentContract?: AgentContract;
126
142
  toolBudget?: ToolBudgetConfig;
127
143
  }
128
144
 
@@ -137,15 +153,19 @@ export interface SubagentParamsLike {
137
153
  agent?: string;
138
154
  task?: string;
139
155
  message?: string;
156
+ steeringRecovery?: boolean;
140
157
  chain?: ChainStep[];
141
158
  tasks?: TaskParam[];
142
159
  concurrency?: number;
143
160
  worktree?: boolean;
144
161
  context?: "fresh" | "fork";
145
162
  async?: boolean;
163
+ foregroundOnly?: boolean;
146
164
  timeoutMs?: number;
147
165
  maxRuntimeMs?: number;
148
166
  turnBudget?: TurnBudgetConfig;
167
+ /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
168
+ enforceHardTurnLimit?: boolean;
149
169
  toolBudget?: ToolBudgetConfig;
150
170
  clarify?: boolean;
151
171
  share?: boolean;
@@ -156,14 +176,20 @@ export interface SubagentParamsLike {
156
176
  artifacts?: boolean;
157
177
  includeProgress?: boolean;
158
178
  model?: string;
179
+ thinking?: string | false;
180
+ scope?: string;
181
+ target?: string;
159
182
  skill?: string | string[] | boolean;
160
183
  output?: string | boolean;
161
184
  outputMode?: "inline" | "file-only";
185
+ outputSchema?: JsonSchemaObject;
162
186
  agentScope?: unknown;
163
187
  chainDir?: string;
164
188
  acceptance?: AcceptanceInput;
189
+ agentContract?: AgentContract;
165
190
  schedule?: string;
166
191
  scheduleName?: string;
192
+ additional?: number;
167
193
  }
168
194
 
169
195
  interface ExecutorDeps {
@@ -171,7 +197,9 @@ interface ExecutorDeps {
171
197
  state: SubagentState;
172
198
  config: ExtensionConfig;
173
199
  asyncByDefault: boolean;
200
+ waitToolEnabled?: boolean;
174
201
  handleScheduledRunAction?: (params: SubagentParamsLike, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
202
+ watchdog?: MainWatchdogRuntime;
175
203
  tempArtifactsDir: string;
176
204
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
177
205
  expandTilde: (p: string) => string;
@@ -180,6 +208,9 @@ interface ExecutorDeps {
180
208
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
181
209
  }
182
210
 
211
+ type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
212
+ type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
213
+
183
214
  interface ExecutionContextData {
184
215
  params: SubagentParamsLike;
185
216
  effectiveCwd: string;
@@ -192,8 +223,8 @@ interface ExecutionContextData {
192
223
  sessionRoot: string;
193
224
  sessionDirForIndex: (idx?: number) => string;
194
225
  sessionFileForIndex: (idx?: number) => string | undefined;
195
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
196
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
226
+ sessionFileForTask: ForkSessionFileForTask;
227
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
197
228
  artifactConfig: ArtifactConfig;
198
229
  artifactsDir: string;
199
230
  backgroundRequestedWhileClarifying: boolean;
@@ -205,9 +236,12 @@ interface ExecutionContextData {
205
236
  deadlineAt?: number;
206
237
  turnBudget?: ResolvedTurnBudget;
207
238
  toolBudget?: ResolvedToolBudget;
239
+ allowZeroToolBudget?: boolean;
208
240
  configToolBudget?: ResolvedToolBudget;
209
241
  contextPolicy: AgentDefaultContextPolicy;
210
242
  modelScope?: ModelScopeConfig;
243
+ parentSessionId: string | null;
244
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
211
245
  }
212
246
 
213
247
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -263,22 +297,34 @@ function trustedSessionRootsForStatus(ctx: ExtensionContext, deps: ExecutorDeps)
263
297
  return [...new Set(roots)];
264
298
  }
265
299
 
266
- function reserveSubagentSpawns(input: { state: SubagentState; config: ExtensionConfig; sessionId: string | null; requested: number; mode: "single" | "parallel" | "chain" }): AgentToolResult<Details> | undefined {
267
- if (input.requested <= 0) return undefined;
268
- if (input.state.subagentSpawns?.sessionId !== input.sessionId) {
269
- input.state.subagentSpawns = { sessionId: input.sessionId, count: 0 };
270
- }
271
- const maxSpawns = resolveMaxSubagentSpawnsPerSession(input.config.maxSubagentSpawnsPerSession);
272
- const used = input.state.subagentSpawns.count;
273
- if (used + input.requested > maxSpawns) {
274
- return {
275
- content: [{ type: "text", text: `Subagent spawn limit reached for this session (${used}/${maxSpawns} used, ${input.requested} requested). Complete the work directly or start a new session.` }],
276
- isError: true,
277
- details: { mode: input.mode, results: [] },
278
- };
279
- }
280
- input.state.subagentSpawns.count = used + input.requested;
281
- return undefined;
300
+ function spawnBudgetErrorResult(message: string, mode: "single" | "parallel" | "chain"): AgentToolResult<Details> {
301
+ return {
302
+ content: [{ type: "text", text: message }],
303
+ isError: true,
304
+ details: { mode, results: [] },
305
+ };
306
+ }
307
+
308
+ function withSpawnBudgetStatus(
309
+ result: AgentToolResult<Details>,
310
+ state: SubagentState,
311
+ config: ExtensionConfig,
312
+ sessionId: string | null,
313
+ ): AgentToolResult<Details> {
314
+ const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId);
315
+ return {
316
+ ...result,
317
+ content: result.content.map((item, index) => index === 0 && item.type === "text"
318
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
319
+ : item),
320
+ details: { ...result.details, spawnBudget },
321
+ };
322
+ }
323
+
324
+ function hasActiveSubagentChildren(state: SubagentState): boolean {
325
+ if (state.subagentInProgress || state.foregroundControls.size > 0) return true;
326
+ const isActive = (status: string) => status === "queued" || status === "running";
327
+ return [...state.asyncJobs.values(), ...(state.fleetJobs?.values() ?? [])].some((job) => isActive(job.status));
282
328
  }
283
329
 
284
330
  function countRequestedSubagentSpawns(params: SubagentParamsLike, config: ExtensionConfig): number {
@@ -315,13 +361,28 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
315
361
  function trimRememberedForegroundRuns(state: SubagentState): void {
316
362
  if (!state.foregroundRuns) return;
317
363
  while (state.foregroundRuns.size > 50) {
318
- const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
319
- if (!oldest) break;
320
- state.foregroundRuns.delete(oldest.runId);
364
+ const oldestTerminal = [...state.foregroundRuns.values()]
365
+ .filter((run) => !run.children.some((child) => child.status === "detached"))
366
+ .sort((left, right) => left.updatedAt - right.updatedAt)[0];
367
+ if (!oldestTerminal) break;
368
+ state.foregroundRuns.delete(oldestTerminal.runId);
321
369
  }
322
370
  }
323
371
 
324
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
372
+ function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) {
373
+ return {
374
+ ...(progress?.activityState ? { activityState: progress.activityState } : {}),
375
+ ...(progress?.lastActivityAt !== undefined ? { lastActivityAt: progress.lastActivityAt } : {}),
376
+ ...(progress?.currentTool ? { currentTool: progress.currentTool } : {}),
377
+ ...(progress?.currentToolStartedAt !== undefined ? { currentToolStartedAt: progress.currentToolStartedAt } : {}),
378
+ ...(progress?.currentPath ? { currentPath: progress.currentPath } : {}),
379
+ ...(progress?.turnCount !== undefined ? { turnCount: progress.turnCount } : {}),
380
+ ...(progress?.tokens !== undefined ? { tokens: progress.tokens } : {}),
381
+ ...(progress?.toolCount !== undefined ? { toolCount: progress.toolCount } : {}),
382
+ };
383
+ }
384
+
385
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
325
386
  state.foregroundRuns ??= new Map();
326
387
  const previous = state.foregroundRuns.get(input.runId);
327
388
  const updatedAt = Date.now();
@@ -329,20 +390,30 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
329
390
  runId: input.runId,
330
391
  mode: input.mode,
331
392
  cwd: input.cwd,
393
+ ...(input.sessionId ? { sessionId: input.sessionId } : {}),
332
394
  updatedAt,
333
395
  children: input.results.map((result, index) => {
334
396
  const child = {
335
397
  agent: result.agent,
336
398
  index,
399
+ ...(result.context ? { context: result.context } : {}),
337
400
  status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
401
+ ...foregroundChildActivityFromProgress(result.progress),
338
402
  updatedAt,
339
403
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
404
+ ...(result.error ? { error: result.error } : {}),
340
405
  ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}),
406
+ ...(result.outputMode ? { outputMode: result.outputMode } : {}),
407
+ ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}),
408
+ ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}),
341
409
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
342
410
  ...(result.artifactPaths ? { artifactPaths: result.artifactPaths } : {}),
343
411
  ...(result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}),
344
412
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
345
413
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
414
+ ...(result.acceptance ? { acceptance: result.acceptance } : {}),
415
+ ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
416
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
346
417
  };
347
418
  const recovered = previous?.children[index];
348
419
  return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
@@ -351,12 +422,35 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
351
422
  trimRememberedForegroundRuns(state);
352
423
  }
353
424
 
354
- function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; index: number; result: SingleResult }): void {
425
+ function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void {
426
+ const run = state.foregroundRuns?.get(event.runId);
427
+ if (!run) return;
428
+ const index = event.index ?? (run.children.length === 1 ? run.children[0]?.index : undefined);
429
+ if (index === undefined) return;
430
+ const child = run.children[index];
431
+ if (!child || child.status !== "detached") return;
432
+ const updatedAt = event.ts;
433
+ run.updatedAt = updatedAt;
434
+ run.children[index] = {
435
+ ...child,
436
+ activityState: event.to,
437
+ updatedAt,
438
+ ...(event.elapsedMs !== undefined ? { lastActivityAt: event.ts - event.elapsedMs } : {}),
439
+ ...(event.currentTool ? { currentTool: event.currentTool } : {}),
440
+ ...(event.currentToolDurationMs !== undefined ? { currentToolStartedAt: event.ts - event.currentToolDurationMs } : {}),
441
+ ...(event.currentPath ? { currentPath: event.currentPath } : {}),
442
+ ...(event.turns !== undefined ? { turnCount: event.turns } : {}),
443
+ ...(event.tokens !== undefined ? { tokens: event.tokens } : {}),
444
+ ...(event.toolCount !== undefined ? { toolCount: event.toolCount } : {}),
445
+ };
446
+ }
447
+
448
+ function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): void {
355
449
  state.foregroundRuns ??= new Map();
356
450
  const updatedAt = Date.now();
357
451
  let run = state.foregroundRuns.get(input.runId);
358
452
  if (!run) {
359
- run = { runId: input.runId, mode: input.mode, cwd: input.cwd, updatedAt, children: [] };
453
+ run = { runId: input.runId, mode: input.mode, cwd: input.cwd, ...(input.sessionId ? { sessionId: input.sessionId } : {}), updatedAt, children: [] };
360
454
  state.foregroundRuns.set(input.runId, run);
361
455
  }
362
456
  run.updatedAt = updatedAt;
@@ -365,38 +459,83 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
365
459
  ...child,
366
460
  agent: input.result.agent,
367
461
  index: input.index,
368
- status: resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
462
+ ...(input.result.context ? { context: input.result.context } : {}),
463
+ status: input.result.acceptance?.status === "rejected"
464
+ ? "failed"
465
+ : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
466
+ ...foregroundChildActivityFromProgress(input.result.progress),
369
467
  updatedAt,
370
468
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
469
+ ...(input.result.error ? { error: input.result.error } : {}),
371
470
  ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}),
471
+ outputMode: input.result.outputMode,
472
+ savedOutputPath: input.result.savedOutputPath,
473
+ outputSaveError: input.result.outputSaveError,
372
474
  ...(input.result.sessionFile ? { sessionFile: input.result.sessionFile } : {}),
373
475
  ...(input.result.artifactPaths ? { artifactPaths: input.result.artifactPaths } : {}),
374
476
  ...(input.result.transcriptPath ? { transcriptPath: input.result.transcriptPath } : {}),
375
477
  ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
376
478
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
479
+ ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
480
+ ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
481
+ ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
377
482
  };
378
483
  trimRememberedForegroundRuns(state);
484
+ const output = getSingleResultOutput(input.result).trim();
485
+ const success = input.result.exitCode === 0 && input.result.acceptance?.status !== "rejected";
486
+ const summary = !success && input.result.error
487
+ ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
488
+ : output || input.result.error || "Detached child exited without final output.";
489
+ // A detached callback may outlive its extension runtime. Stale sessions are
490
+ // intentionally dropped rather than routed through a replacement runtime.
491
+ if (!input.sessionId || input.sessionId !== state.currentSessionId) return;
492
+ input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, {
493
+ id: `${input.runId}:${input.index}`,
494
+ runId: input.runId,
495
+ source: "foreground",
496
+ mode: input.mode,
497
+ agent: input.result.agent,
498
+ success,
499
+ summary,
500
+ exitCode: input.result.exitCode,
501
+ state: success ? "complete" : "failed",
502
+ timestamp: updatedAt,
503
+ cwd: input.cwd,
504
+ sessionFile: input.result.sessionFile,
505
+ sessionId: input.sessionId,
506
+ taskIndex: input.index,
507
+ });
379
508
  }
380
509
 
381
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
510
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
382
511
  const requested = (params.id ?? params.runId)?.trim();
383
- if (!requested || !state.foregroundRuns?.size) return undefined;
384
- const direct = state.foregroundRuns.get(requested);
385
- const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
512
+ if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
513
+ const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
514
+ const direct = sessionRuns.find((run) => run.runId === requested);
515
+ const matches = direct ? [direct] : sessionRuns.filter((run) => run.runId.startsWith(requested));
386
516
  if (matches.length === 0) return undefined;
387
517
  if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
388
518
  const run = matches[0]!;
519
+ if (run.children.some((child) => child.status === "detached")) throw new Error(`Foreground run '${run.runId}' is detached for intercom coordination and cannot be revived safely while any child may still be live. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); use status to recover the result and do not launch a replacement while it remains detached.`);
389
520
  if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
390
521
  const index = params.index ?? 0;
391
522
  if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
392
523
  if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
393
524
  const child = run.children[index]!;
394
- if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
395
525
  if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
396
526
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
397
527
  const sessionFile = path.resolve(child.sessionFile);
398
528
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
399
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
529
+ return {
530
+ runId: run.runId,
531
+ mode: run.mode,
532
+ state: "complete",
533
+ agent: child.agent,
534
+ index,
535
+ cwd: run.cwd,
536
+ sessionFile,
537
+ ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
538
+ };
400
539
  }
401
540
 
402
541
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -408,9 +547,9 @@ type NestedResumeSourceTarget = {
408
547
  state: "complete" | "failed" | "paused";
409
548
  agent: string;
410
549
  index: number;
411
- intercomTarget: string;
412
550
  cwd?: string;
413
551
  sessionFile: string;
552
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
414
553
  };
415
554
  type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
416
555
 
@@ -449,7 +588,13 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
449
588
  foregroundError = error;
450
589
  }
451
590
  try {
452
- asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) };
591
+ asyncTarget = {
592
+ source: "async",
593
+ ...resolveAsyncResumeTarget(params, {}, {
594
+ requireSessionFile: options.asyncRequireSessionFile,
595
+ sessionId: state.currentSessionId ?? undefined,
596
+ }),
597
+ };
453
598
  } catch (error) {
454
599
  asyncError = error;
455
600
  }
@@ -481,6 +626,7 @@ function getAsyncInterruptTarget(
481
626
  state: SubagentState,
482
627
  runId: string | undefined,
483
628
  location?: { asyncDir: string | null; resolvedId?: string },
629
+ options: { fallbackToNewest?: boolean } = {},
484
630
  ): { asyncId: string; asyncDir: string } | undefined {
485
631
  if (location?.asyncDir) {
486
632
  return {
@@ -491,6 +637,7 @@ function getAsyncInterruptTarget(
491
637
  if (runId) {
492
638
  const direct = state.asyncJobs.get(runId);
493
639
  if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
640
+ if (options.fallbackToNewest === false) return undefined;
494
641
  }
495
642
  let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined;
496
643
  for (const job of state.asyncJobs.values()) {
@@ -567,64 +714,48 @@ function interruptAsyncRun(
567
714
  }
568
715
  }
569
716
 
570
- function steerAsyncRun(input: {
571
- state: SubagentState;
572
- runId: string;
573
- message: string;
574
- index?: number;
575
- kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
576
- location: { asyncDir: string | null; resolvedId?: string };
577
- }): AgentToolResult<Details> {
578
- if (!input.location.asyncDir) {
717
+ function stopAsyncRun(
718
+ state: SubagentState,
719
+ runId: string | undefined,
720
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean,
721
+ location?: { asyncDir: string | null; resolvedId?: string },
722
+ ): AgentToolResult<Details> | null {
723
+ const target = getAsyncInterruptTarget(state, runId, location, { fallbackToNewest: false });
724
+ if (!target) return null;
725
+ const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
726
+ if (state.currentSessionId && status?.sessionId !== state.currentSessionId) {
579
727
  return {
580
- content: [{ type: "text", text: `Async run '${input.runId}' has no live run directory to steer.` }],
728
+ content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
581
729
  isError: true,
582
730
  details: { mode: "management", results: [] },
583
731
  };
584
732
  }
585
- const status = reconcileAsyncRun(input.location.asyncDir, { kill: input.kill }).status;
586
733
  if (!status || (status.state !== "running" && status.state !== "queued")) {
587
734
  return {
588
- content: [{ type: "text", text: `Async run '${input.runId}' is not running or queued and cannot be steered.` }],
735
+ content: [{ type: "text", text: `No running or queued async run was found for '${runId ?? "current"}'.` }],
589
736
  isError: true,
590
737
  details: { mode: "management", results: [] },
591
738
  };
592
739
  }
593
- const steps = status.steps ?? [];
594
- if (input.index !== undefined) {
595
- if (input.index < 0 || input.index >= steps.length) {
596
- return {
597
- content: [{ type: "text", text: `Async run '${status.runId}' has ${steps.length} children. Index ${input.index} is out of range.` }],
598
- isError: true,
599
- details: { mode: "management", results: [] },
600
- };
601
- }
602
- const targetStep = steps[input.index];
603
- if (targetStep && targetStep.status !== "running" && targetStep.status !== "pending") {
604
- return {
605
- content: [{ type: "text", text: `Async run '${status.runId}' child ${input.index} is ${targetStep.status} and cannot be steered.` }],
606
- isError: true,
607
- details: { mode: "management", results: [] },
608
- };
609
- }
610
- } else {
611
- const running = steps.filter((step) => step.status === "running");
612
- if (running.length === 0 && steps.length > 1) {
613
- return {
614
- content: [{ type: "text", text: `Async run '${status.runId}' has no running child yet. Provide index to steer a queued child.` }],
615
- isError: true,
616
- details: { mode: "management", results: [] },
617
- };
740
+ try {
741
+ deliverStopRequest({ asyncDir: target.asyncDir, pid: typeof status.pid === "number" ? status.pid : undefined, kill, source: "stop-action" });
742
+ const tracked = state.asyncJobs.get(target.asyncId);
743
+ if (tracked) {
744
+ tracked.activityState = undefined;
745
+ tracked.updatedAt = Date.now();
618
746
  }
747
+ return {
748
+ content: [{ type: "text", text: `Stop requested for async run ${target.asyncId}.` }],
749
+ details: { mode: "management", results: [] },
750
+ };
751
+ } catch (error) {
752
+ const message = error instanceof Error ? error.message : String(error);
753
+ return {
754
+ content: [{ type: "text", text: `Failed to stop async run ${target.asyncId}: ${message}` }],
755
+ isError: true,
756
+ details: { mode: "management", results: [] },
757
+ };
619
758
  }
620
- requestAsyncSteer(input.location.asyncDir, { message: input.message, targetIndex: input.index, source: "steer-action" });
621
- const tracked = input.state.asyncJobs.get(status.runId);
622
- if (tracked) tracked.updatedAt = Date.now();
623
- const childText = input.index !== undefined ? ` child ${input.index}` : " running child";
624
- return {
625
- content: [{ type: "text", text: `Steering queued for async run ${status.runId}${childText}. Delivery requires a live Pi child session that supports mid-run steering.` }],
626
- details: { mode: "management", results: [] },
627
- };
628
759
  }
629
760
 
630
761
  function duplicateNames(names: string[]): string[] {
@@ -754,6 +885,7 @@ function appendStepToAsyncChain(input: {
754
885
  currentModelProvider: input.ctx.model?.provider,
755
886
  currentModel: input.ctx.model,
756
887
  modelScope: discoveredForAppend.modelScope,
888
+ interactive: input.ctx.hasUI,
757
889
  };
758
890
  const built = buildAsyncRunnerSteps(resolved.id, {
759
891
  chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
@@ -766,8 +898,11 @@ function appendStepToAsyncChain(input: {
766
898
  chainSkills,
767
899
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
768
900
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
901
+ waitToolEnabled: input.deps.waitToolEnabled,
902
+ contextForAgent: contextPolicy.contextForAgent,
769
903
  asyncDir: resolved.location.asyncDir,
770
904
  validateOutputBindings: false,
905
+ capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
771
906
  });
772
907
  if ("error" in built) {
773
908
  return {
@@ -858,6 +993,7 @@ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: s
858
993
  function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
859
994
  const run = match.match.run;
860
995
  if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
996
+ if (run.state === "stopped") throw new Error(`Nested run '${run.id}' was stopped and cannot be resumed. Start a new run instead.`);
861
997
  const agent = nestedRunAgent(run);
862
998
  if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
863
999
  const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
@@ -869,9 +1005,9 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
869
1005
  state,
870
1006
  agent,
871
1007
  index: 0,
872
- intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0),
873
1008
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
874
1009
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1010
+ ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
875
1011
  };
876
1012
  }
877
1013
 
@@ -913,7 +1049,7 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
913
1049
  }
914
1050
  }
915
1051
 
916
- function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult<Details> | undefined {
1052
+ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details> | undefined> {
917
1053
  const run = input.target.match.run;
918
1054
  const asyncDir = resolveNestedAsyncDir(input.target.match.rootRunId, run);
919
1055
  if (!asyncDir) return undefined;
@@ -925,8 +1061,32 @@ function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind:
925
1061
  const step = steps[input.index];
926
1062
  if (step && step.status !== "running" && step.status !== "pending") return { content: [{ type: "text", text: `Nested async run ${run.id} child ${input.index} is ${step.status} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } };
927
1063
  }
928
- requestAsyncSteer(asyncDir, { message: input.message, targetIndex: input.index, source: "nested-steer" });
929
- return { content: [{ type: "text", text: `Steering queued for nested async run ${run.id}. Delivery requires a live Pi child session that supports mid-run steering.` }], details: { mode: "management", results: [] } };
1064
+ const runningIndexes = steps
1065
+ .map((step, index) => step.status === "running" ? index : undefined)
1066
+ .filter((index): index is number => index !== undefined);
1067
+ const effectiveTargetIndex = input.index ?? (status.mode === "single" && runningIndexes.length === 0 && steps[0]?.status === "pending" ? 0 : undefined);
1068
+ const targetIndexes = effectiveTargetIndex !== undefined ? [effectiveTargetIndex] : runningIndexes;
1069
+ if (targetIndexes.length === 0) return { content: [{ type: "text", text: `Nested async run ${run.id} has no running child to steer.` }], isError: true, details: { mode: "management", results: [] } };
1070
+ const requestId = randomUUID();
1071
+ try {
1072
+ requestAsyncSteer(asyncDir, {
1073
+ message: input.message,
1074
+ ...(effectiveTargetIndex !== undefined ? { targetIndex: effectiveTargetIndex } : { targetIndexes }),
1075
+ source: "nested-steer",
1076
+ id: requestId,
1077
+ });
1078
+ } catch (error) {
1079
+ return { content: [{ type: "text", text: `Failed to queue steering for nested async run ${run.id}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "management", results: [] } };
1080
+ }
1081
+ const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
1082
+ if (targets.every((target) => target.state === "scheduled")) {
1083
+ const scheduled = { requestId, state: "scheduled" as const, sourceRunId: run.id, targets };
1084
+ return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1085
+ }
1086
+ const waited = await waitForSteeringAction({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal });
1087
+ const result = waited ?? { requestId, state: "pending" as const, sourceRunId: run.id, targets };
1088
+ const stateText = result.state === "delivered" ? "delivered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending";
1089
+ return { content: [{ type: "text", text: `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).` }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } };
930
1090
  }
931
1091
 
932
1092
  async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
@@ -948,10 +1108,10 @@ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { ki
948
1108
  return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
949
1109
  }
950
1110
 
951
- function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult<Details> {
1111
+ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details>> {
952
1112
  const run = input.target.match.run;
953
1113
  if (run.state !== "running" && run.state !== "queued") return { content: [{ type: "text", text: `Nested run ${run.id} is ${run.state} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } };
954
- const direct = directNestedAsyncSteer(input);
1114
+ const direct = await directNestedAsyncSteer(input);
955
1115
  if (direct) return direct;
956
1116
  return { content: [{ type: "text", text: `Nested run ${run.id} is not a live async Pi child session with a steering inbox. action='steer' cannot target foreground nested runs.` }], isError: true, details: { mode: "management", results: [] } };
957
1117
  }
@@ -961,6 +1121,7 @@ async function resumeAsyncRun(input: {
961
1121
  requestCwd: string;
962
1122
  ctx: ExtensionContext;
963
1123
  deps: ExecutorDeps;
1124
+ absoluteDeadlineAt?: number;
964
1125
  }): Promise<AgentToolResult<Details>> {
965
1126
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
966
1127
  const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
@@ -971,6 +1132,15 @@ async function resumeAsyncRun(input: {
971
1132
  details: { mode: "management", results: [] },
972
1133
  };
973
1134
  }
1135
+ const acceptanceErrors = validateExecutionAcceptance(input.params);
1136
+ if (acceptanceErrors.length > 0) {
1137
+ return {
1138
+ content: [{ type: "text", text: `Cannot resume: ${acceptanceErrors.join(" ")}` }],
1139
+ isError: true,
1140
+ details: { mode: "management", results: [] },
1141
+ };
1142
+ }
1143
+ input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
974
1144
 
975
1145
  let target: ResumeSourceTarget;
976
1146
  const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
@@ -1009,34 +1179,14 @@ async function resumeAsyncRun(input: {
1009
1179
  }
1010
1180
 
1011
1181
  if (target.kind === "live" && !attachChain) {
1012
- const interrupt = interruptLiveAsyncResumeTarget({
1013
- target,
1014
- state: input.deps.state,
1015
- kill: input.deps.kill,
1016
- resultsDir: RESULTS_DIR,
1017
- });
1018
- if (!interrupt.ok) {
1019
- return {
1020
- content: [{ type: "text", text: interrupt.message }],
1021
- isError: true,
1022
- details: { mode: "management", results: [] },
1023
- };
1024
- }
1025
- const delivered = await deliverSubagentIntercomMessageEvent(
1026
- input.deps.pi.events,
1027
- target.intercomTarget,
1028
- `Follow-up for async run ${target.runId} (${target.agent}):\n\n${followUp}`,
1029
- 500,
1030
- { source: "async-resume", runId: target.runId, agent: target.agent, index: target.index },
1031
- );
1032
- if (delivered) {
1033
- return {
1034
- content: [{ type: "text", text: [`Interrupted live async child, then delivered follow-up.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
1035
- details: { mode: "management", results: [] },
1036
- };
1037
- }
1038
1182
  return {
1039
- content: [{ type: "text", text: [`Async child appears live but its intercom target is not registered.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`, `Wait for completion, then retry action='resume'.`].join("\n") }],
1183
+ content: [{
1184
+ type: "text",
1185
+ text: [
1186
+ `Async child '${target.runId}' index ${target.index} is still running. action='resume' only revives paused, completed, or failed children.`,
1187
+ `Send live input with subagent({ action: "steer", id: "${target.runId}", index: ${target.index}, message: "..." }).`,
1188
+ ].join("\n"),
1189
+ }],
1040
1190
  isError: true,
1041
1191
  details: { mode: "management", results: [] },
1042
1192
  };
@@ -1066,7 +1216,18 @@ async function resumeAsyncRun(input: {
1066
1216
  const agents = intercomBridge.active
1067
1217
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1068
1218
  : discoveredAgents;
1069
- const agentConfig = agents.find((agent) => agent.name === target.agent);
1219
+ const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1220
+ const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent);
1221
+ const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1222
+ name: recoveryDescriptor.agent,
1223
+ description: "Persisted async recovery contract",
1224
+ systemPrompt: "",
1225
+ systemPromptMode: recoveryDescriptor.systemPromptMode,
1226
+ inheritProjectContext: recoveryDescriptor.inheritProjectContext,
1227
+ inheritSkills: recoveryDescriptor.inheritSkills,
1228
+ source: "project",
1229
+ filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, ".pi-subagents-recovery-agent"),
1230
+ } : undefined);
1070
1231
  if (!agentConfig) {
1071
1232
  return {
1072
1233
  content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
@@ -1091,14 +1252,17 @@ async function resumeAsyncRun(input: {
1091
1252
  };
1092
1253
  }
1093
1254
  const runId = randomUUID().slice(0, 8);
1094
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1255
+ const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1095
1256
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1096
1257
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1258
+ const workflowTask = (input.params.task ?? followUp) || undefined;
1259
+ const goal = resolveAsyncEventGoal(workflowTask, attachChain);
1097
1260
  const chain = wrapChainTasksForFork(attachChain, contextPolicy);
1098
1261
  const normalized = normalizeSkillInput(input.params.skill);
1099
1262
  const result = executeAsyncChain(runId, {
1100
1263
  chain,
1101
- task: (input.params.task ?? followUp) || undefined,
1264
+ task: workflowTask,
1265
+ goal,
1102
1266
  attachRoot: {
1103
1267
  runId: target.runId,
1104
1268
  asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
@@ -1116,17 +1280,20 @@ async function resumeAsyncRun(input: {
1116
1280
  currentModelProvider: input.ctx.model?.provider,
1117
1281
  currentModel: input.ctx.model,
1118
1282
  modelScope,
1283
+ interactive: input.ctx.hasUI,
1119
1284
  },
1120
1285
  availableModels,
1121
1286
  cwd: effectiveCwd,
1122
1287
  maxOutput: input.params.maxOutput,
1123
- artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd),
1288
+ artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir),
1124
1289
  artifactConfig,
1125
1290
  shareEnabled: input.params.share === true,
1126
1291
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1127
1292
  chainSkills: normalized === false ? [] : (normalized ?? []),
1293
+ agentContract: input.params.agentContract,
1128
1294
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1129
1295
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1296
+ waitToolEnabled: input.deps.waitToolEnabled,
1130
1297
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1131
1298
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1132
1299
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
@@ -1134,6 +1301,7 @@ async function resumeAsyncRun(input: {
1134
1301
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1135
1302
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1136
1303
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1304
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1137
1305
  });
1138
1306
  if (result.isError) return result;
1139
1307
  const attachedId = result.details.asyncId ?? runId;
@@ -1144,17 +1312,19 @@ async function resumeAsyncRun(input: {
1144
1312
  result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
1145
1313
  `Status if needed: subagent({ action: "status", id: "${attachedId}" })`,
1146
1314
  ].filter((line): line is string => Boolean(line));
1147
- return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
1315
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1148
1316
  }
1149
1317
 
1150
1318
  const runId = randomUUID().slice(0, 8);
1151
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1152
- const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
1319
+ const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1320
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1321
+ const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1153
1322
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1154
1323
  const result = executeAsyncSingle(runId, {
1155
1324
  agent: target.agent,
1156
1325
  task: buildRevivedAsyncTask(target, followUp),
1157
- agentConfig,
1326
+ goal: followUp,
1327
+ agentConfig: recoveryAgentConfig,
1158
1328
  ctx: {
1159
1329
  pi: input.deps.pi,
1160
1330
  cwd: input.requestCwd,
@@ -1163,23 +1333,45 @@ async function resumeAsyncRun(input: {
1163
1333
  currentModelProvider: input.ctx.model?.provider,
1164
1334
  currentModel: input.ctx.model,
1165
1335
  modelScope,
1336
+ interactive: input.ctx.hasUI,
1166
1337
  },
1167
1338
  cwd: effectiveCwd,
1168
- maxOutput: input.params.maxOutput,
1339
+ maxOutput: input.params.maxOutput ?? recoveryDescriptor?.maxOutput,
1169
1340
  artifactsDir,
1170
1341
  artifactConfig,
1171
- shareEnabled: input.params.share === true,
1342
+ shareEnabled: recoveryDescriptor?.share ?? input.params.share === true,
1172
1343
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1344
+ ...(recoveryDescriptor?.sessionDir ? { sessionDir: recoveryDescriptor.sessionDir } : {}),
1173
1345
  sessionFile: target.sessionFile,
1346
+ revivalLease: {
1347
+ sessionFile: target.sessionFile,
1348
+ runId,
1349
+ sourceRunId: target.runId,
1350
+ ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
1351
+ },
1352
+ modelOverride: input.params.model ?? recoveryDescriptor?.model ?? target.model,
1353
+ thinkingOverride: input.params.model ? undefined : recoveryDescriptor?.thinking ?? target.thinking,
1174
1354
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1175
- maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1355
+ maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1356
+ waitToolEnabled: input.deps.waitToolEnabled,
1176
1357
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1177
1358
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1178
1359
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1179
- controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
1360
+ controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1180
1361
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1181
1362
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1182
1363
  availableModels,
1364
+ output: recoveryDescriptor?.outputPath,
1365
+ outputMode: recoveryDescriptor?.outputMode,
1366
+ ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}),
1367
+ ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}),
1368
+ ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
1369
+ ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
1370
+ ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
1371
+ ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1372
+ ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1373
+ ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1374
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1183
1375
  });
1184
1376
  if (result.isError) return result;
1185
1377
 
@@ -1195,7 +1387,7 @@ async function resumeAsyncRun(input: {
1195
1387
  revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : undefined,
1196
1388
  `Status if needed: subagent({ action: "status", id: "${revivedId}" })`,
1197
1389
  ].filter((line): line is string => Boolean(line));
1198
- return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
1390
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1199
1391
  }
1200
1392
 
1201
1393
  function resultSummaryForIntercom(result: SingleResult): string {
@@ -1213,19 +1405,22 @@ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string
1213
1405
  if (output && output !== error.trim()) {
1214
1406
  lines.push("", "Output:", output);
1215
1407
  }
1216
- if (result.artifactPaths?.outputPath) {
1408
+ if (result.artifactPaths?.outputPath && fs.existsSync(result.artifactPaths.outputPath)) {
1217
1409
  lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
1218
1410
  }
1219
1411
  return lines.join("\n");
1220
1412
  }
1221
1413
 
1222
- function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
1223
- return (event) => emitControlNotification({
1224
- pi: deps.pi,
1225
- controlConfig: data.controlConfig,
1226
- intercomBridge: data.intercomBridge,
1227
- event,
1228
- });
1414
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1415
+ return (event) => {
1416
+ applyControlEventToRememberedForegroundRun(deps.state, event);
1417
+ emitControlNotification({
1418
+ pi: deps.pi,
1419
+ controlConfig: data.controlConfig,
1420
+ intercomBridge: data.intercomBridge,
1421
+ event,
1422
+ });
1423
+ };
1229
1424
  }
1230
1425
 
1231
1426
  async function emitForegroundResultIntercom(input: {
@@ -1236,6 +1431,7 @@ async function emitForegroundResultIntercom(input: {
1236
1431
  results: SingleResult[];
1237
1432
  chainSteps?: number;
1238
1433
  nestedChildren?: NestedRunSummary[];
1434
+ parallelHandoff?: Details["parallelHandoff"];
1239
1435
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
1240
1436
  if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
1241
1437
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
@@ -1259,6 +1455,7 @@ async function emitForegroundResultIntercom(input: {
1259
1455
  source: "foreground",
1260
1456
  children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
1261
1457
  ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
1458
+ ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
1262
1459
  });
1263
1460
  const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
1264
1461
  if (!delivered) return null;
@@ -1281,6 +1478,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1281
1478
  results: input.details.results,
1282
1479
  ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
1283
1480
  ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
1481
+ ...(input.details.parallelHandoff ? { parallelHandoff: input.details.parallelHandoff } : {}),
1284
1482
  });
1285
1483
  if (!payload) return null;
1286
1484
  return {
@@ -1413,25 +1611,6 @@ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanou
1413
1611
  return null;
1414
1612
  }
1415
1613
 
1416
- function validateExecutionAcceptance(params: SubagentParamsLike): string[] {
1417
- const errors: string[] = [];
1418
- errors.push(...validateAcceptanceInput(params.acceptance, "acceptance"));
1419
- for (const [index, task] of (params.tasks ?? []).entries()) {
1420
- errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
1421
- }
1422
- for (const [stepIndex, step] of (params.chain ?? []).entries()) {
1423
- errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`));
1424
- if (isParallelStep(step)) {
1425
- for (const [taskIndex, task] of step.parallel.entries()) {
1426
- errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
1427
- }
1428
- } else if (isDynamicParallelStep(step)) {
1429
- errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
1430
- }
1431
- }
1432
- return errors;
1433
- }
1434
-
1435
1614
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1436
1615
  if ((params.chain?.length ?? 0) > 0) return "chain";
1437
1616
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
@@ -1441,7 +1620,8 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1441
1620
 
1442
1621
  interface AgentDefaultContextPolicy {
1443
1622
  params: SubagentParamsLike;
1444
- contextForAgent(agentName: string): "fresh" | "fork";
1623
+ contextForAgent(agentName: string): ContextMode;
1624
+ contextSummary?: ContextSummary;
1445
1625
  usesFork: boolean;
1446
1626
  }
1447
1627
 
@@ -1450,12 +1630,15 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
1450
1630
  return resolveExplicitContextPolicy(params);
1451
1631
  }
1452
1632
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
1453
- const contextForAgent = (agentName: string): "fresh" | "fork" =>
1633
+ const contextForAgent = (agentName: string): ContextMode =>
1454
1634
  byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
1455
- const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
1635
+ const requestedAgentNames = collectRequestedAgentNames(params);
1636
+ const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
1637
+ const usesFork = contextSummary === "fork" || contextSummary === "mixed";
1456
1638
  return {
1457
- params: usesFork ? { ...params, context: "fork" } : params,
1639
+ params,
1458
1640
  contextForAgent,
1641
+ contextSummary,
1459
1642
  usesFork,
1460
1643
  };
1461
1644
  }
@@ -1465,6 +1648,7 @@ function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultC
1465
1648
  return {
1466
1649
  params,
1467
1650
  contextForAgent: () => context,
1651
+ contextSummary: context,
1468
1652
  usesFork: context === "fork",
1469
1653
  };
1470
1654
  }
@@ -1481,8 +1665,12 @@ function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: st
1481
1665
  return contextPolicy.contextForAgent(agentName) === "fork";
1482
1666
  }
1483
1667
 
1668
+ function summarizeResultContext(details: Details, fallback: ContextSummary | undefined): ContextSummary | undefined {
1669
+ return summarizeContextModes(details.results.map((result) => result.context)) ?? fallback;
1670
+ }
1671
+
1484
1672
  function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
1485
- return withForkContext(
1673
+ return withResolvedContext(
1486
1674
  {
1487
1675
  content: [{ type: "text", text: message }],
1488
1676
  isError: true,
@@ -1492,6 +1680,25 @@ function buildRequestedModeError(params: SubagentParamsLike, message: string): A
1492
1680
  );
1493
1681
  }
1494
1682
 
1683
+ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike {
1684
+ if ((params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || !params.agent) return params;
1685
+ const agent = agents.find((candidate) => candidate.name === params.agent);
1686
+ if (!agent) return params;
1687
+ return {
1688
+ ...params,
1689
+ ...(params.async === undefined && agent.defaultAsync !== undefined ? { async: agent.defaultAsync } : {}),
1690
+ ...(params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs !== undefined
1691
+ ? { timeoutMs: agent.defaultTimeoutMs }
1692
+ : {}),
1693
+ ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined
1694
+ ? { turnBudget: agent.defaultTurnBudget }
1695
+ : {}),
1696
+ ...(params.acceptance === undefined && agent.defaultAcceptance !== undefined
1697
+ ? { acceptance: agent.defaultAcceptance }
1698
+ : {}),
1699
+ };
1700
+ }
1701
+
1495
1702
  function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
1496
1703
  const rawTimeout = params.timeoutMs;
1497
1704
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -1508,22 +1715,12 @@ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: num
1508
1715
  return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1509
1716
  }
1510
1717
 
1511
- function resolveTurnBudget(params: SubagentParamsLike, config: ExtensionConfig): { turnBudget?: ResolvedTurnBudget; error?: string } {
1512
- const raw = params.turnBudget ?? config.turnBudget;
1513
- if (raw === undefined) return {};
1514
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: "turnBudget must be an object with maxTurns and optional graceTurns." };
1515
- if (typeof raw.maxTurns !== "number" || !Number.isInteger(raw.maxTurns) || raw.maxTurns < 1) {
1516
- return { error: "turnBudget.maxTurns must be an integer >= 1." };
1517
- }
1518
- const graceTurns = raw.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
1519
- if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) {
1520
- return { error: "turnBudget.graceTurns must be an integer >= 0." };
1521
- }
1522
- return { turnBudget: { maxTurns: raw.maxTurns, graceTurns } };
1523
- }
1524
-
1525
- function resolveToolBudget(raw: unknown, label = "toolBudget"): { toolBudget?: ResolvedToolBudget; error?: string } {
1526
- const resolved = validateToolBudgetConfig(raw, label);
1718
+ function resolveToolBudget(
1719
+ raw: unknown,
1720
+ label = "toolBudget",
1721
+ options?: { minimumHard?: 0 | 1 },
1722
+ ): { toolBudget?: ResolvedToolBudget; error?: string } {
1723
+ const resolved = validateToolBudgetConfig(raw, label, options);
1527
1724
  return { toolBudget: resolved.budget, error: resolved.error };
1528
1725
  }
1529
1726
 
@@ -1593,35 +1790,50 @@ function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?:
1593
1790
  return { params };
1594
1791
  }
1595
1792
 
1596
- function withForkContext(
1793
+ function withResolvedContext(
1597
1794
  result: AgentToolResult<Details>,
1598
- context: SubagentParamsLike["context"],
1795
+ fallback: ContextSummary | undefined,
1599
1796
  ): AgentToolResult<Details> {
1600
- if (context !== "fork" || !result.details) return result;
1797
+ if (!result.details) return result;
1798
+ const context = summarizeResultContext(result.details, fallback);
1799
+ if (!context) return result;
1601
1800
  return {
1602
1801
  ...result,
1603
1802
  details: {
1604
1803
  ...result.details,
1605
- context: "fork",
1804
+ context,
1606
1805
  },
1607
1806
  };
1608
1807
  }
1609
1808
 
1610
- function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult<Details> {
1809
+ function withForkThinkingNotes(
1810
+ result: AgentToolResult<Details>,
1811
+ downgrades: Map<number, string>,
1812
+ ): AgentToolResult<Details> {
1813
+ if (downgrades.size === 0) return result;
1814
+ const children = [...downgrades.entries()]
1815
+ .sort(([a], [b]) => a - b)
1816
+ .map(([index, agent]) => `${agent} (child ${index})`)
1817
+ .join(", ");
1818
+ const note = `Note: fork context forced thinking off for ${children}. The forked transcript contained signed Anthropic thinking blocks that were sanitized, and Anthropic children cannot resume such a transcript with thinking enabled. Use context: "fresh" when an Anthropic child needs thinking.`;
1819
+ return { ...result, content: [...result.content, { type: "text", text: note }] };
1820
+ }
1821
+
1822
+ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, contextSummary?: ContextSummary): AgentToolResult<Details> {
1611
1823
  const message = error instanceof Error ? error.message : String(error);
1612
- return withForkContext(
1824
+ return withResolvedContext(
1613
1825
  {
1614
1826
  content: [{ type: "text", text: message }],
1615
1827
  isError: true,
1616
1828
  details: { mode: getRequestedModeLabel(params), results: [] },
1617
1829
  },
1618
- params.context,
1830
+ contextSummary,
1619
1831
  );
1620
1832
  }
1621
1833
 
1622
1834
  function collectChainSessionFiles(
1623
1835
  chain: ChainStep[],
1624
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1836
+ sessionFileForTask: ForkSessionFileForTask,
1625
1837
  dynamicFanoutMaxItems?: number,
1626
1838
  ): (string | undefined)[] {
1627
1839
  const sessionFiles: (string | undefined)[] = [];
@@ -1629,7 +1841,7 @@ function collectChainSessionFiles(
1629
1841
  for (const step of chain) {
1630
1842
  if (isParallelStep(step)) {
1631
1843
  for (const task of step.parallel) {
1632
- sessionFiles.push(sessionFileForTask(task.agent, flatIndex));
1844
+ sessionFiles.push(sessionFileForTask(task.agent, flatIndex, task.model));
1633
1845
  flatIndex++;
1634
1846
  }
1635
1847
  continue;
@@ -1637,12 +1849,13 @@ function collectChainSessionFiles(
1637
1849
  if (isDynamicParallelStep(step)) {
1638
1850
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1639
1851
  for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1640
- sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex));
1852
+ sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex, step.parallel.model));
1641
1853
  flatIndex++;
1642
1854
  }
1643
1855
  continue;
1644
1856
  }
1645
- sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
1857
+ const sequential = step as SequentialStep;
1858
+ sessionFiles.push(sessionFileForTask(sequential.agent, flatIndex, sequential.model));
1646
1859
  flatIndex++;
1647
1860
  }
1648
1861
  return sessionFiles;
@@ -1650,7 +1863,7 @@ function collectChainSessionFiles(
1650
1863
 
1651
1864
  function collectChainThinkingOverrides(
1652
1865
  chain: ChainStep[],
1653
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined,
1866
+ thinkingOverrideForTask: ForkThinkingOverrideForTask,
1654
1867
  dynamicFanoutMaxItems?: number,
1655
1868
  ): (AgentConfig["thinking"] | undefined)[] {
1656
1869
  const thinkingOverrides: (AgentConfig["thinking"] | undefined)[] = [];
@@ -1658,7 +1871,7 @@ function collectChainThinkingOverrides(
1658
1871
  for (const step of chain) {
1659
1872
  if (isParallelStep(step)) {
1660
1873
  for (const task of step.parallel) {
1661
- thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex));
1874
+ thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex, task.model));
1662
1875
  flatIndex++;
1663
1876
  }
1664
1877
  continue;
@@ -1666,17 +1879,42 @@ function collectChainThinkingOverrides(
1666
1879
  if (isDynamicParallelStep(step)) {
1667
1880
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1668
1881
  for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1669
- thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex));
1882
+ thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex, step.parallel.model));
1670
1883
  flatIndex++;
1671
1884
  }
1672
1885
  continue;
1673
1886
  }
1674
- thinkingOverrides.push(thinkingOverrideForTask((step as SequentialStep).agent, flatIndex));
1887
+ const sequential = step as SequentialStep;
1888
+ thinkingOverrides.push(thinkingOverrideForTask(sequential.agent, flatIndex, sequential.model));
1675
1889
  flatIndex++;
1676
1890
  }
1677
1891
  return thinkingOverrides;
1678
1892
  }
1679
1893
 
1894
+ function firstChainAgent(chain: ChainStep[]): string | undefined {
1895
+ const first = chain[0];
1896
+ if (!first) return undefined;
1897
+ if (isParallelStep(first)) return first.parallel[0]?.agent;
1898
+ if (isDynamicParallelStep(first)) return first.parallel.agent;
1899
+ return (first as SequentialStep).agent;
1900
+ }
1901
+
1902
+ function firstRawChainTask(chain: ChainStep[]): string | undefined {
1903
+ const first = chain[0];
1904
+ if (!first) return undefined;
1905
+ if (isParallelStep(first)) return first.parallel[0]?.task;
1906
+ if (isDynamicParallelStep(first)) return first.parallel.task;
1907
+ return (first as SequentialStep).task;
1908
+ }
1909
+
1910
+ function resolveAsyncEventGoal(workflowTask: string | undefined, rawChain: ChainStep[], unwrapForkFallback = false): string {
1911
+ if (workflowTask?.trim()) return workflowTask;
1912
+ const fallback = firstRawChainTask(rawChain) || "";
1913
+ if (!unwrapForkFallback) return fallback;
1914
+ const forkPrefix = `${DEFAULT_FORK_PREAMBLE}\n\nTask:\n`;
1915
+ return fallback.startsWith(forkPrefix) ? fallback.slice(forkPrefix.length) : fallback;
1916
+ }
1917
+
1680
1918
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1681
1919
  return chain.map((step, stepIndex) => {
1682
1920
  if (isParallelStep(step)) {
@@ -1714,17 +1952,17 @@ function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultCo
1714
1952
  function preflightForkSessionsForStaticTasks(
1715
1953
  params: SubagentParamsLike,
1716
1954
  contextPolicy: AgentDefaultContextPolicy,
1717
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1955
+ sessionFileForTask: ForkSessionFileForTask,
1718
1956
  dynamicFanoutMaxItems?: number,
1719
1957
  ): void {
1720
1958
  if (!contextPolicy.usesFork) return;
1721
1959
  if (params.agent) {
1722
- if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0);
1960
+ if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0, params.model);
1723
1961
  return;
1724
1962
  }
1725
1963
  if (params.tasks) {
1726
1964
  params.tasks.forEach((task, index) => {
1727
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index);
1965
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index, task.model);
1728
1966
  });
1729
1967
  return;
1730
1968
  }
@@ -1733,7 +1971,7 @@ function preflightForkSessionsForStaticTasks(
1733
1971
  for (const step of params.chain) {
1734
1972
  if (isParallelStep(step)) {
1735
1973
  for (const task of step.parallel) {
1736
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex);
1974
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex, task.model);
1737
1975
  flatIndex++;
1738
1976
  }
1739
1977
  continue;
@@ -1741,13 +1979,13 @@ function preflightForkSessionsForStaticTasks(
1741
1979
  if (isDynamicParallelStep(step)) {
1742
1980
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1743
1981
  if (shouldForkAgent(contextPolicy, step.parallel.agent)) {
1744
- for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex);
1982
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex, step.parallel.model);
1745
1983
  }
1746
1984
  flatIndex += maxItems;
1747
1985
  continue;
1748
1986
  }
1749
1987
  const sequential = step as SequentialStep;
1750
- if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex);
1988
+ if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex, sequential.model);
1751
1989
  flatIndex++;
1752
1990
  }
1753
1991
  }
@@ -1814,6 +2052,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1814
2052
  currentModelProvider: ctx.model?.provider,
1815
2053
  currentModel: ctx.model,
1816
2054
  modelScope: data.modelScope,
2055
+ interactive: ctx.hasUI,
1817
2056
  };
1818
2057
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1819
2058
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
@@ -1822,22 +2061,20 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1822
2061
  const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
1823
2062
 
1824
2063
  if (hasTasks && params.tasks) {
1825
- const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
1826
- const modelOverrides = params.tasks.map((task, index) =>
1827
- resolveSubagentModelOverride(task.model ?? agentConfigs[index]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: task.model ? "explicit" : "inherited" }),
1828
- );
1829
2064
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1830
2065
  const parallelTasks = params.tasks.map((task, index) => ({
1831
2066
  agent: task.agent,
1832
2067
  task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
1833
2068
  cwd: task.cwd,
1834
- ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
2069
+ ...(task.model !== undefined ? { model: task.model } : {}),
1835
2070
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
1836
- ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
2071
+ ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
1837
2072
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1838
2073
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1839
2074
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1840
2075
  ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
2076
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
2077
+ ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
1841
2078
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
1842
2079
  }));
1843
2080
  return executeAsyncChain(id, {
@@ -1847,6 +2084,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1847
2084
  worktree: params.worktree,
1848
2085
  }],
1849
2086
  resultMode: "parallel",
2087
+ goal: params.tasks[0]?.task ?? "",
1850
2088
  agents,
1851
2089
  ctx: asyncCtx,
1852
2090
  availableModels,
@@ -1857,13 +2095,16 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1857
2095
  shareEnabled,
1858
2096
  sessionRoot,
1859
2097
  chainSkills: [],
1860
- sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1861
- thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
2098
+ sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2099
+ thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2100
+ contextForAgent: contextPolicy.contextForAgent,
1862
2101
  maxSubagentDepth: currentMaxSubagentDepth,
2102
+ waitToolEnabled: deps.waitToolEnabled,
1863
2103
  worktreeSetupHook: deps.config.worktreeSetupHook,
1864
2104
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1865
2105
  worktreeBaseDir: deps.config.worktreeBaseDir,
1866
2106
  controlConfig,
2107
+ agentContract: params.agentContract,
1867
2108
  controlIntercomTarget,
1868
2109
  childIntercomTarget,
1869
2110
  nestedRoute,
@@ -1871,6 +2112,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1871
2112
  turnBudget: data.turnBudget,
1872
2113
  toolBudget: data.toolBudget,
1873
2114
  configToolBudget: data.configToolBudget,
2115
+ capabilityCeiling: data.capabilityCeiling,
1874
2116
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1875
2117
  });
1876
2118
  }
@@ -1878,10 +2120,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1878
2120
  if (hasChain && params.chain) {
1879
2121
  const normalized = normalizeSkillInput(params.skill);
1880
2122
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1881
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2123
+ const rawChain = params.chain as ChainStep[];
2124
+ const chain = wrapChainTasksForFork(rawChain, contextPolicy);
1882
2125
  return executeAsyncChain(id, {
1883
2126
  chain,
1884
2127
  task: params.task,
2128
+ goal: resolveAsyncEventGoal(params.task, rawChain),
1885
2129
  agents,
1886
2130
  ctx: asyncCtx,
1887
2131
  availableModels,
@@ -1894,12 +2138,15 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1894
2138
  chainSkills,
1895
2139
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
1896
2140
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2141
+ contextForAgent: contextPolicy.contextForAgent,
1897
2142
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1898
2143
  maxSubagentDepth: currentMaxSubagentDepth,
2144
+ waitToolEnabled: deps.waitToolEnabled,
1899
2145
  worktreeSetupHook: deps.config.worktreeSetupHook,
1900
2146
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1901
2147
  worktreeBaseDir: deps.config.worktreeBaseDir,
1902
2148
  controlConfig,
2149
+ agentContract: params.agentContract,
1903
2150
  controlIntercomTarget,
1904
2151
  childIntercomTarget,
1905
2152
  nestedRoute,
@@ -1907,6 +2154,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1907
2154
  turnBudget: data.turnBudget,
1908
2155
  toolBudget: data.toolBudget,
1909
2156
  configToolBudget: data.configToolBudget,
2157
+ capabilityCeiling: data.capabilityCeiling,
1910
2158
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1911
2159
  });
1912
2160
  }
@@ -1926,10 +2174,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1926
2174
  const normalizedSkills = normalizeSkillInput(params.skill);
1927
2175
  const skills = normalizedSkills === false ? [] : normalizedSkills;
1928
2176
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
1929
- const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" });
2177
+ const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
1930
2178
  return executeAsyncSingle(id, {
1931
2179
  agent: params.agent!,
1932
2180
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2181
+ goal: params.task ?? "",
1933
2182
  agentConfig: a,
1934
2183
  ctx: asyncCtx,
1935
2184
  availableModels,
@@ -1939,14 +2188,16 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1939
2188
  artifactConfig,
1940
2189
  shareEnabled,
1941
2190
  sessionRoot,
1942
- sessionFile: sessionFileForTask(params.agent!, 0),
2191
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2192
+ context: contextPolicy.contextForAgent(params.agent!),
1943
2193
  skills,
1944
2194
  output: effectiveOutput,
1945
2195
  outputMode: effectiveOutputMode,
1946
2196
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
1947
2197
  modelOverride,
1948
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2198
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
1949
2199
  maxSubagentDepth,
2200
+ waitToolEnabled: deps.waitToolEnabled,
1950
2201
  worktreeSetupHook: deps.config.worktreeSetupHook,
1951
2202
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1952
2203
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -1954,11 +2205,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1954
2205
  controlIntercomTarget,
1955
2206
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1956
2207
  nestedRoute,
2208
+ agentContract: params.agentContract,
2209
+ structuredOutputSchema: params.outputSchema,
1957
2210
  acceptance: params.acceptance,
1958
2211
  timeoutMs: data.timeoutMs,
1959
2212
  turnBudget: data.turnBudget,
1960
2213
  toolBudget: data.toolBudget,
1961
2214
  configToolBudget: data.configToolBudget,
2215
+ capabilityCeiling: data.capabilityCeiling,
1962
2216
  });
1963
2217
  }
1964
2218
 
@@ -2007,6 +2261,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2007
2261
  sessionFileForIndex,
2008
2262
  sessionFileForTask,
2009
2263
  thinkingOverrideForTask,
2264
+ contextForAgent: contextPolicy.contextForAgent,
2010
2265
  artifactsDir,
2011
2266
  artifactConfig,
2012
2267
  includeProgress: params.includeProgress,
@@ -2014,6 +2269,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2014
2269
  onUpdate,
2015
2270
  onControlEvent,
2016
2271
  controlConfig,
2272
+ agentContract: params.agentContract,
2017
2273
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2018
2274
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2019
2275
  foregroundControl,
@@ -2028,10 +2284,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2028
2284
  timeoutMs: data.timeoutMs,
2029
2285
  deadlineAt: data.deadlineAt,
2030
2286
  turnBudget: data.turnBudget,
2031
- onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, index, result }),
2287
+ onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events }),
2032
2288
  toolBudget: data.toolBudget,
2033
2289
  configToolBudget: data.configToolBudget,
2034
2290
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2291
+ capabilityCeiling: data.capabilityCeiling,
2035
2292
  });
2036
2293
 
2037
2294
  if (chainResult.requestedAsync) {
@@ -2051,11 +2308,15 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2051
2308
  currentModelProvider: ctx.model?.provider,
2052
2309
  currentModel: ctx.model,
2053
2310
  modelScope: data.modelScope,
2311
+ interactive: ctx.hasUI,
2054
2312
  };
2055
- const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy);
2313
+ const rawAsyncChain = chainResult.requestedAsync.chain;
2314
+ const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2315
+ const firstAgent = firstChainAgent(rawAsyncChain);
2056
2316
  return executeAsyncChain(id, {
2057
2317
  chain: asyncChain,
2058
2318
  task: params.task,
2319
+ goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
2059
2320
  agents,
2060
2321
  ctx: asyncCtx,
2061
2322
  availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
@@ -2068,12 +2329,15 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2068
2329
  chainSkills: chainResult.requestedAsync.chainSkills,
2069
2330
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2070
2331
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2332
+ contextForAgent: contextPolicy.contextForAgent,
2071
2333
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2072
2334
  maxSubagentDepth: currentMaxSubagentDepth,
2335
+ waitToolEnabled: deps.waitToolEnabled,
2073
2336
  worktreeSetupHook: deps.config.worktreeSetupHook,
2074
2337
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2075
2338
  worktreeBaseDir: deps.config.worktreeBaseDir,
2076
2339
  controlConfig,
2340
+ agentContract: params.agentContract,
2077
2341
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2078
2342
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2079
2343
  nestedRoute: data.nestedRoute,
@@ -2081,6 +2345,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2081
2345
  turnBudget: data.turnBudget,
2082
2346
  toolBudget: data.toolBudget,
2083
2347
  configToolBudget: data.configToolBudget,
2348
+ capabilityCeiling: data.capabilityCeiling,
2084
2349
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2085
2350
  });
2086
2351
  }
@@ -2092,7 +2357,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2092
2357
  rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
2093
2358
  }
2094
2359
  const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
2095
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
2360
+ if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results });
2096
2361
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
2097
2362
  ? await maybeBuildForegroundIntercomReceipt({
2098
2363
  pi: deps.pi,
@@ -2117,16 +2382,19 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2117
2382
  interface ForegroundParallelRunInput {
2118
2383
  tasks: TaskParam[];
2119
2384
  taskTexts: string[];
2385
+ taskDescriptions: string[];
2120
2386
  agents: AgentConfig[];
2121
2387
  ctx: ExtensionContext;
2122
2388
  state: SubagentState;
2123
2389
  intercomEvents: IntercomEventBus;
2390
+ parentSessionId: string | null;
2391
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
2124
2392
  signal: AbortSignal;
2125
2393
  runId: string;
2126
2394
  sessionDirForIndex: (idx?: number) => string | undefined;
2127
2395
  sessionFileForIndex: (idx?: number) => string | undefined;
2128
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
2129
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
2396
+ sessionFileForTask: ForkSessionFileForTask;
2397
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2130
2398
  shareEnabled: boolean;
2131
2399
  artifactConfig: ArtifactConfig;
2132
2400
  artifactsDir: string;
@@ -2135,12 +2403,14 @@ interface ForegroundParallelRunInput {
2135
2403
  paramsCwd: string;
2136
2404
  progressDir: string;
2137
2405
  maxSubagentDepths: number[];
2406
+ waitToolEnabled?: boolean;
2138
2407
  availableModels: ModelInfo[];
2139
2408
  modelScope?: ModelScopeConfig;
2140
2409
  modelOverrides: (string | undefined)[];
2141
2410
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
2142
2411
  firstProgressIndex: number;
2143
2412
  controlConfig: ResolvedControlConfig;
2413
+ contextPolicy: AgentDefaultContextPolicy;
2144
2414
  onControlEvent?: (event: ControlEvent) => void;
2145
2415
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
2146
2416
  orchestratorIntercomTarget?: string;
@@ -2155,8 +2425,8 @@ interface ForegroundParallelRunInput {
2155
2425
  deadlineAt?: number;
2156
2426
  turnBudget?: ResolvedTurnBudget;
2157
2427
  toolBudgets: (ResolvedToolBudget | undefined)[];
2428
+ agentContract?: AgentContract;
2158
2429
  }
2159
-
2160
2430
  function buildParallelModeError(message: string): AgentToolResult<Details> {
2161
2431
  return {
2162
2432
  content: [{ type: "text", text: message }],
@@ -2229,15 +2499,52 @@ function resolveParallelTaskCwd(
2229
2499
  return resolveChildCwd(paramsCwd, task.cwd);
2230
2500
  }
2231
2501
 
2232
- function buildParallelWorktreeSuffix(
2233
- worktreeSetup: WorktreeSetup | undefined,
2234
- artifactsDir: string,
2235
- tasks: TaskParam[],
2236
- ): string {
2237
- if (!worktreeSetup) return "";
2238
- const diffsDir = path.join(artifactsDir, "worktree-diffs");
2239
- const diffs = diffWorktrees(worktreeSetup, tasks.map((task) => task.agent), diffsDir);
2240
- return formatWorktreeDiffSummary(diffs);
2502
+ function finalizeParallelWorktreeHandoff(input: {
2503
+ worktreeSetup: WorktreeSetup;
2504
+ artifactsDir: string;
2505
+ runId: string;
2506
+ cwd: string;
2507
+ tasks: TaskParam[];
2508
+ results: SingleResult[];
2509
+ }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
2510
+ const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
2511
+ const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
2512
+ const cleanup = cleanupWorktrees(input.worktreeSetup);
2513
+ const diffSummary = formatWorktreeDiffSummary(diffs);
2514
+ try {
2515
+ const reference = writeParallelHandoffGroup({
2516
+ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
2517
+ runId: input.runId,
2518
+ mode: "parallel",
2519
+ source: "foreground",
2520
+ cwd: input.cwd,
2521
+ stepIndex: 0,
2522
+ flatStartIndex: 0,
2523
+ setup: input.worktreeSetup,
2524
+ diffs,
2525
+ cleanup,
2526
+ results: input.results.map((result) => ({
2527
+ agent: result.agent,
2528
+ status: resolveSubagentResultStatus({
2529
+ exitCode: result.exitCode,
2530
+ interrupted: result.interrupted,
2531
+ detached: result.detached,
2532
+ state: result.stopped ? "stopped" : undefined,
2533
+ }),
2534
+ summary: resultSummaryForIntercom(result),
2535
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2536
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2537
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2538
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2539
+ })),
2540
+ });
2541
+ return {
2542
+ suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
2543
+ reference,
2544
+ };
2545
+ } catch (error) {
2546
+ return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
2547
+ }
2241
2548
  }
2242
2549
 
2243
2550
  function findDuplicateParallelOutputPath(input: {
@@ -2268,9 +2575,8 @@ function findDuplicateParallelOutputPath(input: {
2268
2575
  async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
2269
2576
  // Pre-warm fork session files sequentially before concurrent dispatch to avoid
2270
2577
  // races where multiple workers simultaneously try to branch the same parent session.
2271
- // sessionFileForIndex caches results, so these calls return instantly inside mapConcurrent.
2272
2578
  for (let i = 0; i < input.tasks.length; i++) {
2273
- input.sessionFileForIndex(i);
2579
+ input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[i]);
2274
2580
  }
2275
2581
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
2276
2582
  const behavior = input.behaviors[index];
@@ -2283,27 +2589,31 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2283
2589
  ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
2284
2590
  : { prefix: "", suffix: "" };
2285
2591
  const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
2592
+ const agentConfig = input.agents.find((agent) => agent.name === task.agent);
2286
2593
  const taskText = injectSingleOutputInstruction(
2287
2594
  `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
2288
2595
  outputPath,
2596
+ agentConfig,
2289
2597
  );
2290
2598
  const interruptController = new AbortController();
2291
2599
  if (input.foregroundControl) {
2292
- input.foregroundControl.currentAgent = task.agent;
2293
- input.foregroundControl.currentIndex = index;
2294
- input.foregroundControl.currentActivityState = undefined;
2295
- input.foregroundControl.updatedAt = Date.now();
2296
- input.foregroundControl.interrupt = () => {
2297
- if (interruptController.signal.aborted) return false;
2298
- interruptController.abort();
2299
- input.foregroundControl!.currentActivityState = undefined;
2300
- input.foregroundControl!.updatedAt = Date.now();
2301
- return true;
2302
- };
2600
+ beginForegroundChild(input.foregroundControl, {
2601
+ index,
2602
+ agent: task.agent,
2603
+ description: input.taskDescriptions[index],
2604
+ interrupt: () => {
2605
+ if (interruptController.signal.aborted) return false;
2606
+ interruptController.abort();
2607
+ return true;
2608
+ },
2609
+ });
2303
2610
  }
2304
- const agentConfig = input.agents.find((agent) => agent.name === task.agent);
2611
+ const structuredRuntime = task.outputSchema
2612
+ ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2613
+ : undefined;
2305
2614
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2306
2615
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2616
+ context: input.contextPolicy.contextForAgent(task.agent),
2307
2617
  cwd: taskCwd,
2308
2618
  signal: input.signal,
2309
2619
  interruptSignal: interruptController.signal,
@@ -2312,7 +2622,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2312
2622
  runId: input.runId,
2313
2623
  index,
2314
2624
  sessionDir: input.sessionDirForIndex(index),
2315
- sessionFile: input.sessionFileForTask(task.agent, index),
2625
+ sessionFile: input.sessionFileForTask(task.agent, index, input.modelOverrides[index]),
2316
2626
  share: input.shareEnabled,
2317
2627
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
2318
2628
  artifactConfig: input.artifactConfig,
@@ -2320,18 +2630,22 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2320
2630
  outputPath,
2321
2631
  outputMode: behavior?.outputMode,
2322
2632
  maxSubagentDepth: input.maxSubagentDepths[index],
2633
+ waitToolEnabled: input.waitToolEnabled,
2634
+ capabilityCeiling: input.capabilityCeiling,
2323
2635
  controlConfig: input.controlConfig,
2324
2636
  onControlEvent: input.onControlEvent,
2325
- onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, index, result }),
2637
+ onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
2326
2638
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
2327
2639
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
2328
2640
  nestedRoute: input.foregroundControl?.nestedRoute,
2329
2641
  modelOverride: input.modelOverrides[index],
2330
- thinkingOverride: input.thinkingOverrideForTask(task.agent, index),
2642
+ thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]),
2331
2643
  availableModels: input.availableModels,
2332
2644
  preferredModelProvider: input.ctx.model?.provider,
2333
2645
  modelScope: input.modelScope,
2334
2646
  skills: effectiveSkills === false ? [] : effectiveSkills,
2647
+ structuredOutput: structuredRuntime,
2648
+ agentContract: task.agentContract ?? input.agentContract,
2335
2649
  acceptance: task.acceptance,
2336
2650
  acceptanceContext: { mode: "parallel" },
2337
2651
  timeoutMs: input.timeoutMs,
@@ -2343,18 +2657,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2343
2657
  const stepResults = progressUpdate.details?.results || [];
2344
2658
  const stepProgress = progressUpdate.details?.progress || [];
2345
2659
  if (input.foregroundControl && stepProgress.length > 0) {
2346
- const current = stepProgress[0];
2347
- input.foregroundControl.currentAgent = task.agent;
2348
- input.foregroundControl.currentIndex = index;
2349
- input.foregroundControl.currentActivityState = current?.activityState;
2350
- input.foregroundControl.lastActivityAt = current?.lastActivityAt;
2351
- input.foregroundControl.currentTool = current?.currentTool;
2352
- input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
2353
- input.foregroundControl.currentPath = current?.currentPath;
2354
- input.foregroundControl.turnCount = current?.turnCount;
2355
- input.foregroundControl.tokens = current?.tokens;
2356
- input.foregroundControl.toolCount = current?.toolCount;
2357
- input.foregroundControl.updatedAt = Date.now();
2660
+ updateForegroundChild(input.foregroundControl, index, stepProgress[0]);
2358
2661
  }
2359
2662
  if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
2360
2663
  if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
@@ -2373,10 +2676,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2373
2676
  }
2374
2677
  : undefined,
2375
2678
  }).finally(() => {
2376
- if (input.foregroundControl?.currentIndex === index) {
2377
- input.foregroundControl.interrupt = undefined;
2378
- input.foregroundControl.updatedAt = Date.now();
2379
- }
2679
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2380
2680
  });
2381
2681
  }, input.globalSemaphore);
2382
2682
  }
@@ -2453,15 +2753,15 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2453
2753
  normalizeSkillInput(t.skill),
2454
2754
  );
2455
2755
  const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
2456
- ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
2756
+ ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
2457
2757
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
2458
2758
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
2459
2759
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
2460
2760
  ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
2461
- ...(task.model ? { model: task.model } : {}),
2761
+ ...(task.model !== undefined ? { model: task.model } : {}),
2462
2762
  }));
2463
2763
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
2464
- resolveSubagentModelOverride(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: behaviorOverrides[i]?.model ? "explicit" : "inherited" }),
2764
+ resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope }),
2465
2765
  );
2466
2766
 
2467
2767
  if (params.clarify === true && ctx.hasUI) {
@@ -2495,8 +2795,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2495
2795
  taskTexts = result.templates;
2496
2796
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
2497
2797
  const override = result.behaviorOverrides[i];
2498
- if (override?.model) {
2499
- modelOverrides[i] = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" });
2798
+ if (override?.model !== undefined) {
2799
+ modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2500
2800
  behaviorOverrides[i]!.model = override.model;
2501
2801
  }
2502
2802
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -2525,6 +2825,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2525
2825
  currentModelProvider: ctx.model?.provider,
2526
2826
  currentModel: ctx.model,
2527
2827
  modelScope: data.modelScope,
2828
+ interactive: ctx.hasUI,
2528
2829
  };
2529
2830
  const parallelTasks = tasks.map((t, i) => {
2530
2831
  const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
@@ -2533,19 +2834,22 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2533
2834
  agent: t.agent,
2534
2835
  task: taskText,
2535
2836
  cwd: t.cwd,
2536
- ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
2837
+ ...(behaviorOverrides[i]?.model !== undefined ? { model: behaviorOverrides[i]!.model } : {}),
2537
2838
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
2538
2839
  ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
2539
2840
  ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
2540
2841
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
2541
2842
  ...(progress !== undefined ? { progress } : {}),
2542
2843
  ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}),
2844
+ ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
2543
2845
  ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
2846
+ ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
2544
2847
  };
2545
2848
  });
2546
2849
  return executeAsyncChain(id, {
2547
2850
  chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
2548
2851
  resultMode: "parallel",
2852
+ goal: taskTexts[0] ?? "",
2549
2853
  agents,
2550
2854
  ctx: asyncCtx,
2551
2855
  availableModels,
@@ -2556,23 +2860,35 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2556
2860
  shareEnabled,
2557
2861
  sessionRoot,
2558
2862
  chainSkills: [],
2559
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2560
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
2863
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
2864
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
2865
+ contextForAgent: contextPolicy.contextForAgent,
2561
2866
  maxSubagentDepth: currentMaxSubagentDepth,
2867
+ waitToolEnabled: deps.waitToolEnabled,
2562
2868
  worktreeSetupHook: deps.config.worktreeSetupHook,
2563
2869
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2564
2870
  worktreeBaseDir: deps.config.worktreeBaseDir,
2565
2871
  controlConfig,
2872
+ agentContract: params.agentContract,
2566
2873
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2567
2874
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2875
+ nestedRoute: data.nestedRoute,
2568
2876
  timeoutMs: data.timeoutMs,
2569
2877
  turnBudget: data.turnBudget,
2878
+ toolBudget: data.toolBudget,
2879
+ configToolBudget: data.configToolBudget,
2570
2880
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2571
2881
  });
2572
2882
  }
2573
2883
  }
2574
2884
 
2575
- const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
2885
+ const behaviors = tasks.map((task, index) => {
2886
+ let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
2887
+ if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
2888
+ behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
2889
+ }
2890
+ return behavior;
2891
+ });
2576
2892
  const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
2577
2893
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
2578
2894
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
@@ -2588,6 +2904,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2588
2904
  );
2589
2905
  if (errorResult) return errorResult;
2590
2906
 
2907
+ let worktreeFinalized = false;
2591
2908
  try {
2592
2909
  const outputBaseDir = path.join(artifactsDir, "outputs", runId);
2593
2910
  const duplicateOutputError = findDuplicateParallelOutputPath({
@@ -2610,6 +2927,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2610
2927
  const parallelProgressDir = path.join(artifactsDir, "progress", runId);
2611
2928
  if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
2612
2929
 
2930
+ const taskDescriptions = taskTexts.map((taskText) => taskText.trim());
2613
2931
  for (let i = 0; i < taskTexts.length; i++) {
2614
2932
  if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
2615
2933
  }
@@ -2618,10 +2936,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2618
2936
  const results = await runForegroundParallelTasks({
2619
2937
  tasks,
2620
2938
  taskTexts,
2939
+ taskDescriptions,
2621
2940
  agents,
2622
2941
  ctx,
2623
2942
  state: deps.state,
2624
2943
  intercomEvents: deps.pi.events,
2944
+ parentSessionId: data.parentSessionId,
2945
+ capabilityCeiling: data.capabilityCeiling,
2625
2946
  signal,
2626
2947
  runId,
2627
2948
  sessionDirForIndex,
@@ -2641,6 +2962,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2641
2962
  behaviors,
2642
2963
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
2643
2964
  controlConfig,
2965
+ contextPolicy,
2644
2966
  onControlEvent,
2645
2967
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2646
2968
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
@@ -2648,6 +2970,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2648
2970
  concurrencyLimit: parallelConcurrency,
2649
2971
  globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
2650
2972
  maxSubagentDepths,
2973
+ waitToolEnabled: deps.waitToolEnabled,
2651
2974
  liveResults,
2652
2975
  liveProgress,
2653
2976
  onUpdate,
@@ -2656,6 +2979,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2656
2979
  deadlineAt,
2657
2980
  turnBudget: data.turnBudget,
2658
2981
  toolBudgets,
2982
+ agentContract: params.agentContract,
2659
2983
  });
2660
2984
  for (let i = 0; i < results.length; i++) {
2661
2985
  const run = results[i]!;
@@ -2671,6 +2995,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2671
2995
  updateForegroundNestedProjection(foregroundControl);
2672
2996
  attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
2673
2997
  }
2998
+ let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
2999
+ if (worktreeSetup) {
3000
+ worktreeFinalized = true;
3001
+ handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3002
+ }
2674
3003
  const interrupted = results.find((result) => result.interrupted);
2675
3004
  const details = compactForegroundDetails({
2676
3005
  mode: "parallel",
@@ -2680,8 +3009,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2680
3009
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2681
3010
  totalChildUsage: sumResultsUsage(results),
2682
3011
  totalCost: sumResultsCost(results),
3012
+ ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
2683
3013
  });
2684
- rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
3014
+ rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
2685
3015
  if (interrupted) {
2686
3016
  return {
2687
3017
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
@@ -2692,7 +3022,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2692
3022
  const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
2693
3023
  if (detached) {
2694
3024
  return {
2695
- content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
3025
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
2696
3026
  details,
2697
3027
  };
2698
3028
  }
@@ -2713,7 +3043,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2713
3043
  };
2714
3044
  }
2715
3045
 
2716
- const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
3046
+ const worktreeSuffix = handoff?.suffix ?? "";
2717
3047
  const ok = results.filter((result) => result.exitCode === 0).length;
2718
3048
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
2719
3049
  const aggregatedOutput = aggregateParallelOutputs(
@@ -2737,7 +3067,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2737
3067
  details,
2738
3068
  };
2739
3069
  } finally {
2740
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3070
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
2741
3071
  }
2742
3072
  }
2743
3073
 
@@ -2773,17 +3103,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2773
3103
  };
2774
3104
  }
2775
3105
  const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
2776
- if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error));
3106
+ if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
2777
3107
 
2778
3108
  const currentProvider = ctx.model?.provider;
2779
3109
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2780
3110
  let task = params.task ?? "";
2781
- let modelOverride: string | undefined = resolveSubagentModelOverride(
2782
- (params.model as string | undefined) ?? agentConfig.model,
3111
+ let modelOverride: string | undefined = resolveEffectiveSubagentModel(
3112
+ params.model as string | undefined,
3113
+ agentConfig.model,
2783
3114
  ctx.model,
2784
3115
  availableModels,
2785
3116
  currentProvider,
2786
- { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" },
3117
+ { scope: data.modelScope },
2787
3118
  );
2788
3119
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
2789
3120
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -2820,7 +3151,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2820
3151
 
2821
3152
  task = result.templates[0]!;
2822
3153
  const override = result.behaviorOverrides[0];
2823
- if (override?.model) modelOverride = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" });
3154
+ if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2824
3155
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
2825
3156
  if (override?.skills !== undefined) skillOverride = override.skills;
2826
3157
 
@@ -2841,10 +3172,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2841
3172
  currentModelProvider: ctx.model?.provider,
2842
3173
  currentModel: ctx.model,
2843
3174
  modelScope: data.modelScope,
3175
+ interactive: ctx.hasUI,
2844
3176
  };
2845
3177
  return executeAsyncSingle(id, {
2846
3178
  agent: params.agent!,
2847
3179
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3180
+ goal: task,
2848
3181
  agentConfig,
2849
3182
  ctx: asyncCtx,
2850
3183
  availableModels,
@@ -2854,23 +3187,30 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2854
3187
  artifactConfig,
2855
3188
  shareEnabled,
2856
3189
  sessionRoot,
2857
- sessionFile: sessionFileForTask(params.agent!, 0),
3190
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3191
+ context: contextPolicy.contextForAgent(params.agent!),
2858
3192
  skills: skillOverride === false ? [] : skillOverride,
2859
3193
  output: effectiveOutput,
2860
3194
  outputMode: effectiveOutputMode,
2861
3195
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2862
3196
  modelOverride,
2863
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
3197
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2864
3198
  maxSubagentDepth,
3199
+ waitToolEnabled: deps.waitToolEnabled,
2865
3200
  worktreeSetupHook: deps.config.worktreeSetupHook,
2866
3201
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2867
3202
  worktreeBaseDir: deps.config.worktreeBaseDir,
2868
3203
  controlConfig,
2869
3204
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2870
3205
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3206
+ nestedRoute: data.nestedRoute,
3207
+ agentContract: params.agentContract,
3208
+ structuredOutputSchema: params.outputSchema,
3209
+ acceptance: params.acceptance,
2871
3210
  timeoutMs: data.timeoutMs,
2872
3211
  turnBudget: data.turnBudget,
2873
3212
  toolBudget: effectiveToolBudget.toolBudget,
3213
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
2874
3214
  });
2875
3215
  }
2876
3216
  }
@@ -2884,7 +3224,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2884
3224
  if (validationError) {
2885
3225
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
2886
3226
  }
2887
- task = injectSingleOutputInstruction(task, outputPath);
3227
+ const structuredRuntime = params.outputSchema
3228
+ ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
3229
+ : undefined;
3230
+ task = injectSingleOutputInstruction(task, outputPath, agentConfig);
2888
3231
 
2889
3232
  let effectiveSkills: string[] | undefined;
2890
3233
  if (skillOverride === false) {
@@ -2895,90 +3238,77 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2895
3238
  const interruptController = new AbortController();
2896
3239
  const foregroundControl = deps.state.foregroundControls.get(runId);
2897
3240
  if (foregroundControl) {
2898
- foregroundControl.currentAgent = params.agent;
2899
- foregroundControl.currentIndex = 0;
2900
- foregroundControl.currentActivityState = undefined;
2901
- foregroundControl.updatedAt = Date.now();
2902
- foregroundControl.interrupt = () => {
2903
- if (interruptController.signal.aborted) return false;
2904
- interruptController.abort();
2905
- foregroundControl.currentActivityState = undefined;
2906
- foregroundControl.updatedAt = Date.now();
2907
- return true;
2908
- };
3241
+ beginForegroundChild(foregroundControl, {
3242
+ index: 0,
3243
+ agent: params.agent!,
3244
+ description: foregroundControl.description,
3245
+ interrupt: () => {
3246
+ if (interruptController.signal.aborted) return false;
3247
+ interruptController.abort();
3248
+ return true;
3249
+ },
3250
+ });
2909
3251
  }
2910
3252
 
2911
3253
  const forwardSingleUpdate = onUpdate
2912
3254
  ? (update: AgentToolResult<Details>) => {
2913
- if (foregroundControl) {
2914
- const firstProgress = update.details?.progress?.[0];
2915
- foregroundControl.currentAgent = params.agent;
2916
- foregroundControl.currentIndex = firstProgress?.index ?? 0;
2917
- foregroundControl.currentActivityState = firstProgress?.activityState;
2918
- foregroundControl.lastActivityAt = firstProgress?.lastActivityAt;
2919
- foregroundControl.currentTool = firstProgress?.currentTool;
2920
- foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt;
2921
- foregroundControl.currentPath = firstProgress?.currentPath;
2922
- foregroundControl.turnCount = firstProgress?.turnCount;
2923
- foregroundControl.tokens = firstProgress?.tokens;
2924
- foregroundControl.toolCount = firstProgress?.toolCount;
2925
- foregroundControl.updatedAt = Date.now();
2926
- }
3255
+ if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]);
2927
3256
  onUpdate(update);
2928
3257
  }
2929
3258
  : undefined;
2930
3259
 
2931
3260
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
2932
- const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2933
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2934
- cwd: effectiveCwd,
2935
- signal,
2936
- interruptSignal: interruptController.signal,
2937
- allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
2938
- intercomEvents: deps.pi.events,
2939
- runId,
2940
- sessionDir: sessionDirForIndex(0),
2941
- sessionFile: sessionFileForTask(params.agent!, 0),
2942
- share: shareEnabled,
2943
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2944
- artifactConfig,
2945
- maxOutput: params.maxOutput,
2946
- outputPath,
2947
- outputMode: effectiveOutputMode,
2948
- maxSubagentDepth,
2949
- onUpdate: forwardSingleUpdate,
2950
- controlConfig,
2951
- onControlEvent,
2952
- intercomSessionName: childIntercomTarget,
2953
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2954
- nestedRoute: foregroundControl?.nestedRoute,
2955
- index: 0,
2956
- modelOverride,
2957
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2958
- availableModels,
2959
- preferredModelProvider: currentProvider,
2960
- modelScope: data.modelScope,
2961
- skills: effectiveSkills,
2962
- acceptance: params.acceptance,
2963
- acceptanceContext: { mode: "single" },
2964
- onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, index: 0, result }),
2965
- timeoutMs: data.timeoutMs,
2966
- deadlineAt,
2967
- turnBudget: data.turnBudget,
2968
- toolBudget: effectiveToolBudget.toolBudget,
2969
- });
2970
- if (foregroundControl?.currentIndex === 0) {
2971
- foregroundControl.interrupt = undefined;
2972
- foregroundControl.currentActivityState = r.progress?.activityState;
2973
- foregroundControl.lastActivityAt = r.progress?.lastActivityAt;
2974
- foregroundControl.currentTool = r.progress?.currentTool;
2975
- foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt;
2976
- foregroundControl.currentPath = r.progress?.currentPath;
2977
- foregroundControl.turnCount = r.progress?.turnCount;
2978
- foregroundControl.tokens = r.progress?.tokens;
2979
- foregroundControl.toolCount = r.progress?.toolCount;
2980
- foregroundControl.updatedAt = Date.now();
3261
+ let r: Awaited<ReturnType<typeof runSync>>;
3262
+ try {
3263
+ r = await runSync(ctx.cwd, agents, params.agent!, task, {
3264
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3265
+ context: data.contextPolicy.contextForAgent(params.agent!),
3266
+ cwd: effectiveCwd,
3267
+ signal,
3268
+ interruptSignal: interruptController.signal,
3269
+ allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3270
+ intercomEvents: deps.pi.events,
3271
+ runId,
3272
+ sessionDir: sessionDirForIndex(0),
3273
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3274
+ share: shareEnabled,
3275
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3276
+ artifactConfig,
3277
+ maxOutput: params.maxOutput,
3278
+ outputPath,
3279
+ outputMode: effectiveOutputMode,
3280
+ maxSubagentDepth,
3281
+ waitToolEnabled: deps.waitToolEnabled,
3282
+ onUpdate: forwardSingleUpdate,
3283
+ controlConfig,
3284
+ onControlEvent,
3285
+ intercomSessionName: childIntercomTarget,
3286
+ orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3287
+ nestedRoute: foregroundControl?.nestedRoute,
3288
+ index: 0,
3289
+ modelOverride,
3290
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3291
+ availableModels,
3292
+ preferredModelProvider: currentProvider,
3293
+ modelScope: data.modelScope,
3294
+ skills: effectiveSkills,
3295
+ structuredOutput: structuredRuntime,
3296
+ agentContract: params.agentContract,
3297
+ acceptance: params.acceptance,
3298
+ acceptanceContext: { mode: "single" },
3299
+ onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
3300
+ timeoutMs: data.timeoutMs,
3301
+ deadlineAt,
3302
+ turnBudget: data.turnBudget,
3303
+ enforceHardTurnLimit: params.enforceHardTurnLimit,
3304
+ toolBudget: effectiveToolBudget.toolBudget,
3305
+ capabilityCeiling: data.capabilityCeiling,
3306
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3307
+ });
3308
+ } finally {
3309
+ if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
2981
3310
  }
3311
+ if (foregroundControl) finishForegroundChild(foregroundControl, 0);
2982
3312
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
2983
3313
 
2984
3314
  if (r.progress) allProgress.push(r.progress);
@@ -3011,7 +3341,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3011
3341
  totalChildUsage: sumResultsUsage([r]),
3012
3342
  totalCost: sumResultsCost([r]),
3013
3343
  });
3014
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
3344
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3015
3345
 
3016
3346
  if (!r.detached && !r.interrupted) {
3017
3347
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
@@ -3034,7 +3364,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3034
3364
 
3035
3365
  if (r.detached) {
3036
3366
  return {
3037
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
3367
+ content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
3038
3368
  details,
3039
3369
  };
3040
3370
  }
@@ -3098,7 +3428,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3098
3428
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3099
3429
  ctx: ExtensionContext,
3100
3430
  ) => Promise<AgentToolResult<Details>>;
3431
+ /**
3432
+ * Correlated extension-to-extension delegation owns its request IDs and
3433
+ * cancellation controllers, so independent requests may execute concurrently.
3434
+ * The ordinary model-facing tool keeps the one-foreground-call-per-turn guard.
3435
+ */
3436
+ executeDelegated: (
3437
+ id: string,
3438
+ params: SubagentParamsLike,
3439
+ signal: AbortSignal,
3440
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3441
+ ctx: ExtensionContext,
3442
+ ) => Promise<AgentToolResult<Details>>;
3101
3443
  } {
3444
+ const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
3445
+ const delegatedZeroToolBudgets = new WeakSet<object>();
3102
3446
  const execute = async (
3103
3447
  _id: string,
3104
3448
  params: SubagentParamsLike,
@@ -3106,6 +3450,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3106
3450
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3107
3451
  ctx: ExtensionContext,
3108
3452
  ): Promise<AgentToolResult<Details>> => {
3453
+ const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
3454
+ const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
3109
3455
  deps.state.baseCwd = ctx.cwd;
3110
3456
  deps.state.foregroundRuns ??= new Map();
3111
3457
  deps.state.foregroundControls ??= new Map();
@@ -3115,6 +3461,80 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3115
3461
  const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd };
3116
3462
  const action = paramsWithResolvedCwd.action;
3117
3463
  if (action) {
3464
+ if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) {
3465
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
3466
+ return {
3467
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
3468
+ isError: true,
3469
+ details: { mode: "management" as const, results: [] },
3470
+ };
3471
+ }
3472
+ return handleWatchdogToolAction(action, paramsWithResolvedCwd, ctx, deps.watchdog);
3473
+ }
3474
+ if (action === "grant-spawn-budget") {
3475
+ if (deps.allowMutatingManagementActions === false || !ctx.hasUI) {
3476
+ return {
3477
+ content: [{ type: "text", text: "Action 'grant-spawn-budget' is available only from the root interactive parent session." }],
3478
+ isError: true,
3479
+ details: { mode: "management", results: [] },
3480
+ };
3481
+ }
3482
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3483
+ if (!deps.state.currentSessionId) {
3484
+ return {
3485
+ content: [{ type: "text", text: "Action 'grant-spawn-budget' requires an active parent session id." }],
3486
+ isError: true,
3487
+ details: { mode: "management", results: [] },
3488
+ };
3489
+ }
3490
+ if (hasActiveSubagentChildren(deps.state)) {
3491
+ const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId);
3492
+ return {
3493
+ content: [{ type: "text", text: "Spawn budget grants are rejected while current-session children are queued or running. Wait for them to settle, then retry the explicit grant." }],
3494
+ isError: true,
3495
+ details: { mode: "management", results: [], spawnBudget },
3496
+ };
3497
+ }
3498
+ const sessionId = deps.state.currentSessionId;
3499
+ const additional = paramsWithResolvedCwd.additional ?? Number.NaN;
3500
+ const preview = preflightSpawnBudgetGrant(deps.state, deps.config, sessionId, additional);
3501
+ if (preview.error) {
3502
+ return {
3503
+ content: [{ type: "text", text: preview.error }],
3504
+ isError: true,
3505
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3506
+ };
3507
+ }
3508
+ const confirmed = await ctx.ui.confirm(
3509
+ "Grant subagent spawn budget?",
3510
+ `Add ${additional} launches to this logical session?\n\n${formatSpawnBudget(preview.snapshot)}\n\nUsage is not reset. Compaction keeps the same budget; a new parent session starts a fresh one.`,
3511
+ );
3512
+ if (!confirmed) {
3513
+ return {
3514
+ content: [{ type: "text", text: "Spawn budget grant canceled; no capacity was added." }],
3515
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3516
+ };
3517
+ }
3518
+ const currentBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId);
3519
+ if (
3520
+ resolveCurrentSessionId(ctx.sessionManager) !== sessionId
3521
+ || hasActiveSubagentChildren(deps.state)
3522
+ || currentBudget.used !== preview.snapshot.used
3523
+ || currentBudget.granted !== preview.snapshot.granted
3524
+ ) {
3525
+ return {
3526
+ content: [{ type: "text", text: "Spawn budget grant was not applied because the session, budget, or active-child state changed while confirmation was open." }],
3527
+ isError: true,
3528
+ details: { mode: "management", results: [], spawnBudget: currentBudget },
3529
+ };
3530
+ }
3531
+ const granted = grantSpawnBudget(deps.state, deps.config, sessionId, additional);
3532
+ return {
3533
+ content: [{ type: "text", text: granted.error ?? `Spawn budget grant applied: +${additional}. ${formatSpawnBudget(granted.snapshot)}` }],
3534
+ ...(granted.error ? { isError: true } : {}),
3535
+ details: { mode: "management", results: [], spawnBudget: granted.snapshot },
3536
+ };
3537
+ }
3118
3538
  if (action === "doctor") {
3119
3539
  let currentSessionFile: string | null = null;
3120
3540
  let currentSessionId = deps.state.currentSessionId;
@@ -3131,6 +3551,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3131
3551
  } catch (error) {
3132
3552
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3133
3553
  }
3554
+ const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
3134
3555
  return {
3135
3556
  content: [{
3136
3557
  type: "text",
@@ -3147,15 +3568,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3147
3568
  expandTilde: deps.expandTilde,
3148
3569
  }),
3149
3570
  }],
3150
- details: { mode: "management", results: [] },
3571
+ details: { mode: "management", results: [], spawnBudget },
3151
3572
  };
3152
3573
  }
3153
3574
  if (action === "status") {
3575
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3576
+ const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
3577
+ result,
3578
+ deps.state,
3579
+ deps.config,
3580
+ deps.state.currentSessionId,
3581
+ );
3154
3582
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
3155
3583
  const nestedScope = nestedResolutionScopeForExecutor(deps);
3156
3584
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
3157
3585
  if (paramsWithResolvedCwd.view === "fleet") {
3158
- return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots });
3586
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3159
3587
  }
3160
3588
  if (targetRunId) {
3161
3589
  try {
@@ -3164,34 +3592,35 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3164
3592
  const foreground = getForegroundControl(deps.state, resolved.id);
3165
3593
  if (foreground) {
3166
3594
  if (paramsWithResolvedCwd.view === "transcript") {
3167
- return {
3595
+ return withBudget({
3168
3596
  content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Persisted session transcript becomes inspectable after the foreground run completes when sessions are enabled." }],
3169
3597
  details: { mode: "management", results: [] },
3170
- };
3598
+ });
3171
3599
  }
3172
- return foregroundStatusResult(foreground);
3600
+ return withBudget(foregroundStatusResult(foreground));
3173
3601
  }
3174
3602
  }
3175
3603
  } catch (error) {
3176
3604
  const message = error instanceof Error ? error.message : String(error);
3177
- return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
3605
+ return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } });
3178
3606
  }
3179
3607
  } else {
3180
3608
  const foreground = getForegroundControl(deps.state, undefined);
3181
- if (foreground && paramsWithResolvedCwd.view !== "transcript") return foregroundStatusResult(foreground);
3609
+ if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground));
3182
3610
  if (foreground && paramsWithResolvedCwd.view === "transcript") {
3183
- return {
3611
+ return withBudget({
3184
3612
  content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Pass an async run id to inspect a background transcript." }],
3185
3613
  details: { mode: "management", results: [] },
3186
- };
3614
+ });
3187
3615
  }
3188
3616
  }
3189
- return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots });
3617
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3190
3618
  }
3191
3619
  if (action === "resume") {
3192
3620
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
3193
3621
  }
3194
3622
  if (action === "steer") {
3623
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3195
3624
  const message = (paramsWithResolvedCwd.message ?? paramsWithResolvedCwd.task ?? "").trim();
3196
3625
  if (!message) return { content: [{ type: "text", text: "action='steer' requires message." }], isError: true, details: { mode: "management", results: [] } };
3197
3626
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
@@ -3199,7 +3628,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3199
3628
  try {
3200
3629
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3201
3630
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
3202
- return steerAsyncRun({ state: deps.state, runId, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location });
3631
+ return steerAsyncRun({
3632
+ state: deps.state,
3633
+ runId,
3634
+ message,
3635
+ index: paramsWithResolvedCwd.index,
3636
+ kill: deps.kill,
3637
+ location,
3638
+ signal,
3639
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3640
+ ? {}
3641
+ : {
3642
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3643
+ resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }),
3644
+ }
3645
+ ),
3646
+ });
3203
3647
  } catch (error) {
3204
3648
  const text = error instanceof Error ? error.message : String(error);
3205
3649
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3213,10 +3657,31 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3213
3657
  const text = error instanceof Error ? error.message : String(error);
3214
3658
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3215
3659
  }
3216
- if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index });
3660
+ if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
3217
3661
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3218
3662
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
3219
- return steerAsyncRun({ state: deps.state, runId: resolved.id, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location: resolved.location });
3663
+ return steerAsyncRun({
3664
+ state: deps.state,
3665
+ runId: resolved.id,
3666
+ message,
3667
+ index: paramsWithResolvedCwd.index,
3668
+ kill: deps.kill,
3669
+ location: resolved.location,
3670
+ signal,
3671
+ ...(paramsWithResolvedCwd.steeringRecovery === false
3672
+ ? {}
3673
+ : {
3674
+ recover: ({ absoluteDeadlineAt, ...limits }) =>
3675
+ resumeAsyncRun({
3676
+ params: { ...limits, action: "resume", id: resolved!.id, message },
3677
+ requestCwd,
3678
+ ctx,
3679
+ deps,
3680
+ absoluteDeadlineAt,
3681
+ }),
3682
+ }
3683
+ ),
3684
+ });
3220
3685
  }
3221
3686
  if (action === "append-step") {
3222
3687
  return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
@@ -3231,6 +3696,40 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3231
3696
  }
3232
3697
  return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
3233
3698
  }
3699
+ if (action === "stop") {
3700
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3701
+ let resolved: ResolvedSubagentRunId | undefined;
3702
+ if (paramsWithResolvedCwd.dir) {
3703
+ try {
3704
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3705
+ return stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
3706
+ } catch (error) {
3707
+ const text = error instanceof Error ? error.message : String(error);
3708
+ return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3709
+ }
3710
+ }
3711
+ if (!targetRunId) return { content: [{ type: "text", text: "action='stop' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
3712
+ try {
3713
+ resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
3714
+ } catch (error) {
3715
+ const message = error instanceof Error ? error.message : String(error);
3716
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
3717
+ }
3718
+ if (resolved?.kind === "nested") return { content: [{ type: "text", text: "action='stop' supports current-session top-level async runs only." }], isError: true, details: { mode: "management", results: [] } };
3719
+ if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='stop' supports async runs only. Use action='interrupt' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3720
+ const stopResult = stopAsyncRun(
3721
+ deps.state,
3722
+ resolved?.kind === "async" ? resolved.id : targetRunId,
3723
+ deps.kill,
3724
+ resolved?.kind === "async" ? resolved.location : undefined,
3725
+ );
3726
+ if (stopResult) return stopResult;
3727
+ return {
3728
+ content: [{ type: "text", text: "No stoppable async run found in this session." }],
3729
+ isError: true,
3730
+ details: { mode: "management", results: [] },
3731
+ };
3732
+ }
3234
3733
  if (action === "interrupt") {
3235
3734
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3236
3735
  let resolved: ResolvedSubagentRunId | undefined;
@@ -3320,11 +3819,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3320
3819
  depth,
3321
3820
  deps.config.forceTopLevelAsync === true,
3322
3821
  );
3323
- const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
3324
- if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3325
- const turnBudget = resolveTurnBudget(effectiveParams, deps.config);
3326
- if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
3327
- const runToolBudget = resolveToolBudget(effectiveParams.toolBudget, "toolBudget");
3822
+ const runToolBudget = resolveToolBudget(
3823
+ effectiveParams.toolBudget,
3824
+ "toolBudget",
3825
+ allowZeroToolBudget ? { minimumHard: 0 } : undefined,
3826
+ );
3328
3827
  if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error);
3329
3828
  const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget");
3330
3829
  if (configToolBudget.error) return buildRequestedModeError(effectiveParams, configToolBudget.error);
@@ -3336,12 +3835,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3336
3835
  const discovered = deps.discoverAgents(effectiveCwd, scope);
3337
3836
  const discoveredAgents = discovered.agents;
3338
3837
  const modelScope = discovered.modelScope;
3838
+ effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
3839
+ const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
3840
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3841
+ const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
3842
+ if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
3339
3843
  const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
3340
3844
  effectiveParams = contextPolicy.params;
3341
3845
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
3342
3846
  const intercomBridge = resolveIntercomBridge({
3343
3847
  config: deps.config.intercomBridge,
3344
- context: effectiveParams.context,
3848
+ context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
3345
3849
  orchestratorTarget: sessionName,
3346
3850
  });
3347
3851
  const agents = intercomBridge.active
@@ -3370,14 +3874,51 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3370
3874
  );
3371
3875
  if (validationError) return validationError;
3372
3876
 
3877
+ const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
3878
+ const requestedSpawns = countRequestedSubagentSpawns(effectiveParams, deps.config);
3879
+ const spawnPreflight = preflightSpawnBudget(
3880
+ deps.state,
3881
+ deps.config,
3882
+ deps.state.currentSessionId,
3883
+ requestedSpawns,
3884
+ );
3885
+ if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode);
3886
+
3373
3887
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
3374
3888
  let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
3889
+ let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string): void => {};
3890
+ const forkThinkingRequirements = new Map<number, boolean>();
3891
+ const forkThinkingDowngrades = new Map<number, string>();
3375
3892
  try {
3376
- const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined);
3893
+ const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
3894
+ prepareForkThinking = (agentName, index, modelOverride) => {
3895
+ const agentConfig = agents.find((agent) => agent.name === agentName);
3896
+ const primaryModel = resolveEffectiveSubagentModel(
3897
+ modelOverride,
3898
+ agentConfig?.model,
3899
+ ctx.model,
3900
+ forkAvailableModels,
3901
+ ctx.model?.provider,
3902
+ );
3903
+ const candidates = buildModelCandidates(
3904
+ primaryModel,
3905
+ agentConfig?.fallbackModels,
3906
+ forkAvailableModels,
3907
+ ctx.model?.provider,
3908
+ );
3909
+ forkThinkingRequirements.set(
3910
+ index,
3911
+ candidates.length === 0
3912
+ || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, ctx.model?.provider)),
3913
+ );
3914
+ };
3915
+ const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
3916
+ forceThinkingOffForIndex: (index) => forkThinkingRequirements.get(index) ?? true,
3917
+ });
3377
3918
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
3378
3919
  forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
3379
3920
  } catch (error) {
3380
- return toExecutionErrorResult(effectiveParams, error);
3921
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3381
3922
  }
3382
3923
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
3383
3924
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
@@ -3387,8 +3928,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3387
3928
  const artifactConfig: ArtifactConfig = {
3388
3929
  ...DEFAULT_ARTIFACT_CONFIG,
3389
3930
  enabled: effectiveParams.artifacts !== false,
3931
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
3390
3932
  };
3391
- const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
3933
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
3392
3934
 
3393
3935
  let sessionRoot: string;
3394
3936
  if (effectiveParams.sessionDir) {
@@ -3406,39 +3948,48 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3406
3948
  return toExecutionErrorResult(
3407
3949
  effectiveParams,
3408
3950
  new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
3951
+ contextPolicy.contextSummary,
3409
3952
  );
3410
3953
  }
3411
3954
  const sessionDirForIndex = (idx?: number) =>
3412
3955
  path.join(sessionRoot, `run-${idx ?? 0}`);
3413
- const forkSessionFileForTask = (agentName: string, idx?: number) =>
3414
- shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
3415
- const forkThinkingOverrideForTask = (agentName: string, idx?: number) =>
3416
- shouldForkAgent(contextPolicy, agentName) ? forkThinkingOverrideForIndex(idx) : undefined;
3417
- const childSessionFileForTask = (agentName: string, idx?: number) =>
3418
- forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
3956
+ const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride) => {
3957
+ if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
3958
+ prepareForkThinking(agentName, idx, modelOverride);
3959
+ return forkSessionFileForIndex(idx);
3960
+ };
3961
+ const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
3962
+ if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
3963
+ prepareForkThinking(agentName, idx, modelOverride);
3964
+ const override = forkThinkingOverrideForIndex(idx);
3965
+ if (override === "off") forkThinkingDowngrades.set(idx, agentName);
3966
+ return override ?? delegatedThinkingOverride;
3967
+ };
3968
+ const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
3969
+ forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
3419
3970
  const childSessionFileForIndex = (idx?: number) =>
3420
3971
  path.join(sessionDirForIndex(idx), "session.jsonl");
3421
3972
  try {
3422
- preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3973
+ if (!(effectiveParams.clarify === true && ctx.hasUI)) {
3974
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3975
+ }
3423
3976
  } catch (error) {
3424
- return toExecutionErrorResult(effectiveParams, error);
3977
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
3425
3978
  }
3426
3979
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
3427
- if (chainBindingsError) return chainBindingsError;
3980
+ if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
3428
3981
 
3429
3982
  const onUpdateWithContext = onUpdate
3430
- ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
3983
+ ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext(r, contextPolicy.contextSummary))
3431
3984
  : undefined;
3432
3985
 
3433
- const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
3434
- const spawnLimitError = reserveSubagentSpawns({
3435
- state: deps.state,
3436
- config: deps.config,
3437
- sessionId: deps.state.currentSessionId,
3438
- requested: countRequestedSubagentSpawns(effectiveParams, deps.config),
3439
- mode: foregroundMode,
3440
- });
3441
- if (spawnLimitError) return spawnLimitError;
3986
+ const reservation = reserveSpawnBudget(
3987
+ deps.state,
3988
+ deps.config,
3989
+ deps.state.currentSessionId,
3990
+ requestedSpawns,
3991
+ );
3992
+ if (reservation.error) return spawnBudgetErrorResult(reservation.error, foregroundMode);
3442
3993
 
3443
3994
  const execData: ExecutionContextData = {
3444
3995
  params: effectiveParams,
@@ -3464,21 +4015,30 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3464
4015
  timeoutMs: foregroundTimeout.timeoutMs,
3465
4016
  turnBudget: turnBudget.turnBudget,
3466
4017
  toolBudget: runToolBudget.toolBudget,
4018
+ allowZeroToolBudget,
3467
4019
  configToolBudget: configToolBudget.toolBudget,
3468
4020
  contextPolicy,
3469
4021
  modelScope,
4022
+ parentSessionId: deps.state.currentSessionId,
4023
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
3470
4024
  };
3471
4025
 
3472
- const foregroundControl = effectiveAsync
4026
+ const foregroundDescription = effectiveParams.task?.trim()
4027
+ || effectiveParams.tasks?.[0]?.task?.trim()
4028
+ || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
4029
+ const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
3473
4030
  ? undefined
3474
4031
  : {
3475
4032
  runId,
3476
4033
  mode: foregroundMode,
3477
4034
  startedAt: Date.now(),
3478
4035
  updatedAt: Date.now(),
4036
+ cwd: effectiveCwd,
3479
4037
  currentAgent: undefined,
3480
4038
  currentIndex: undefined,
4039
+ description: foregroundDescription,
3481
4040
  currentActivityState: undefined,
4041
+ activeChildren: new Map(),
3482
4042
  nestedRoute,
3483
4043
  interrupt: undefined,
3484
4044
  };
@@ -3493,10 +4053,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3493
4053
  const details = result?.details;
3494
4054
  const state = type === "subagent.nested.started"
3495
4055
  ? "running"
3496
- : result?.isError || details?.results.some((child) => child.exitCode !== 0)
3497
- ? "failed"
3498
- : details?.results.some((child) => child.interrupted)
3499
- ? "paused"
4056
+ : details?.results.some((child) => child.interrupted || child.detached)
4057
+ ? "paused"
4058
+ : result?.isError || details?.results.some((child) => child.exitCode !== 0)
4059
+ ? "failed"
3500
4060
  : "complete";
3501
4061
  const errorText = result?.isError
3502
4062
  ? result.content.find((item) => item.type === "text")?.text
@@ -3536,7 +4096,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3536
4096
  ...(errorText ? { error: errorText } : {}),
3537
4097
  ...(details?.results.length ? { steps: details.results.map((child) => ({
3538
4098
  agent: child.agent,
3539
- status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed",
4099
+ status: child.interrupted || child.detached ? "paused" : child.exitCode === 0 ? "complete" : "failed",
3540
4100
  ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
3541
4101
  ...(child.error ? { error: child.error } : {}),
3542
4102
  })) } : {}),
@@ -3550,7 +4110,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3550
4110
  let nestedForegroundStarted = false;
3551
4111
  try {
3552
4112
  const asyncResult = runAsyncPath(execData, deps);
3553
- if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
4113
+ if (asyncResult) return withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary);
3554
4114
  if (foregroundControl) {
3555
4115
  writeNestedForegroundEvent("subagent.nested.started");
3556
4116
  nestedForegroundStarted = true;
@@ -3558,20 +4118,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3558
4118
  if (hasChain && effectiveParams.chain) {
3559
4119
  const result = await runChainPath(execData, deps);
3560
4120
  writeNestedForegroundEvent("subagent.nested.completed", result);
3561
- return withForkContext(result, effectiveParams.context);
4121
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3562
4122
  }
3563
4123
  if (hasTasks && effectiveParams.tasks) {
3564
4124
  const result = await runParallelPath(execData, deps);
3565
4125
  writeNestedForegroundEvent("subagent.nested.completed", result);
3566
- return withForkContext(result, effectiveParams.context);
4126
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3567
4127
  }
3568
4128
  if (hasSingle) {
3569
4129
  const result = await runSinglePath(execData, deps);
3570
4130
  writeNestedForegroundEvent("subagent.nested.completed", result);
3571
- return withForkContext(result, effectiveParams.context);
4131
+ return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
3572
4132
  }
3573
4133
  } catch (error) {
3574
- const errorResult = toExecutionErrorResult(effectiveParams, error);
4134
+ const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
3575
4135
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
3576
4136
  return errorResult;
3577
4137
  } finally {
@@ -3584,11 +4144,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3584
4144
  }
3585
4145
  }
3586
4146
 
3587
- return withForkContext({
4147
+ return withResolvedContext({
3588
4148
  content: [{ type: "text", text: "Invalid params" }],
3589
4149
  isError: true,
3590
4150
  details: { mode: "single" as const, results: [] },
3591
- }, effectiveParams.context);
4151
+ }, contextPolicy.contextSummary);
3592
4152
  };
3593
4153
 
3594
4154
  const executeWithSingleDispatchGuard = async (
@@ -3600,6 +4160,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3600
4160
  ): Promise<AgentToolResult<Details>> => {
3601
4161
  const requestParams = omitExecutionModeActionAlias(params);
3602
4162
  if (requestParams.action) return execute(id, requestParams, signal, onUpdate, ctx);
4163
+ const { depth } = checkSubagentDepth(deps.config.maxSubagentDepth);
4164
+ const dispatchParams = applyForceTopLevelAsyncOverride(requestParams, depth, deps.config.forceTopLevelAsync === true);
4165
+ const runsForeground = dispatchParams.clarify === true || (dispatchParams.async ?? deps.asyncByDefault) !== true;
4166
+ if (!runsForeground) return execute(id, requestParams, signal, onUpdate, ctx);
3603
4167
  if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(requestParams);
3604
4168
  deps.state.subagentInProgress = true;
3605
4169
  try {
@@ -3609,5 +4173,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3609
4173
  }
3610
4174
  };
3611
4175
 
3612
- return { execute: executeWithSingleDispatchGuard };
4176
+ const executeDelegated = async (
4177
+ id: string,
4178
+ params: SubagentParamsLike,
4179
+ signal: AbortSignal,
4180
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4181
+ ctx: ExtensionContext,
4182
+ ): Promise<AgentToolResult<Details>> => {
4183
+ const delegatedParams = { ...params };
4184
+ const privateParams = delegatedParams as SubagentParamsLike & {
4185
+ delegatedThinkingOverride?: AgentConfig["thinking"];
4186
+ delegatedAllowZeroToolBudget?: true;
4187
+ };
4188
+ const thinkingOverride = privateParams.delegatedThinkingOverride;
4189
+ const allowZeroToolBudget = privateParams.delegatedAllowZeroToolBudget === true;
4190
+ delete privateParams.delegatedThinkingOverride;
4191
+ delete privateParams.delegatedAllowZeroToolBudget;
4192
+ if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
4193
+ if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
4194
+ return execute(id, delegatedParams, signal, onUpdate, ctx);
4195
+ };
4196
+
4197
+ return { execute: executeWithSingleDispatchGuard, executeDelegated };
3613
4198
  }