@selesai/code 0.2.1 → 0.3.1

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 (284) hide show
  1. package/dist/config.d.ts +4 -15
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -45
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +8 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/diagnostics.d.ts +1 -0
  10. package/dist/core/diagnostics.d.ts.map +1 -1
  11. package/dist/core/diagnostics.js.map +1 -1
  12. package/dist/core/model-registry.d.ts +4 -1
  13. package/dist/core/model-registry.d.ts.map +1 -1
  14. package/dist/core/model-registry.js +101 -51
  15. package/dist/core/model-registry.js.map +1 -1
  16. package/dist/core/package-manager.d.ts +10 -0
  17. package/dist/core/package-manager.d.ts.map +1 -1
  18. package/dist/core/package-manager.js +93 -8
  19. package/dist/core/package-manager.js.map +1 -1
  20. package/dist/core/package-manager.test.d.ts +2 -0
  21. package/dist/core/package-manager.test.d.ts.map +1 -0
  22. package/dist/core/package-manager.test.js +20 -0
  23. package/dist/core/package-manager.test.js.map +1 -0
  24. package/dist/core/resource-loader.d.ts +3 -0
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +10 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +1 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/defaults/models.json +64 -113
  32. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  33. package/dist/extensions/caveman/index.js +116 -0
  34. package/dist/extensions/caveman/package.json +8 -0
  35. package/dist/extensions/caveman/test/extension.test.js +185 -0
  36. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  37. package/dist/extensions/copy-turn.ts +2 -2
  38. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  39. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  40. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  41. package/dist/extensions/pi-intercom/config.ts +2 -2
  42. package/dist/extensions/pi-intercom/index.ts +2 -2
  43. package/dist/extensions/pi-intercom/package.json +2 -2
  44. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  45. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  46. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  47. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
  48. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  49. package/dist/extensions/pi-powerline-footer/index.ts +40 -47
  50. package/dist/extensions/pi-powerline-footer/package.json +2 -2
  51. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
  52. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
  53. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
  54. package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
  55. package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
  56. package/dist/extensions/pi-powerline-footer/types.ts +7 -7
  57. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  58. package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
  59. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  60. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  61. package/dist/extensions/pi-subagents/README.md +224 -52
  62. package/dist/extensions/pi-subagents/agents/architect.md +188 -0
  63. package/dist/extensions/pi-subagents/agents/builder.md +120 -0
  64. package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
  65. package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
  66. package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
  67. package/dist/extensions/pi-subagents/install.mjs +93 -0
  68. package/dist/extensions/pi-subagents/package.json +4 -5
  69. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  70. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  71. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  72. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  73. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  74. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  75. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  76. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  77. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  78. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  79. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  80. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  81. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  82. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  83. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  84. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  85. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  86. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  87. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  88. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  89. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  90. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  91. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  92. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  93. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  94. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  95. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  96. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  97. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  98. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  100. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  101. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  103. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  105. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  106. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  107. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  109. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  112. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  113. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  114. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  116. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  117. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  118. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  119. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  120. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  121. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  123. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
  126. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  128. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  129. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  133. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  134. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  135. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  136. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  137. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  138. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  139. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  140. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  141. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  142. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  143. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  144. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  145. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  146. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  147. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  148. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  149. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  150. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  151. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  152. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  153. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  154. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  155. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  156. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  157. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  158. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  159. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  160. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  161. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  162. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  163. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  164. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  165. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  166. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  167. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  168. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  169. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  170. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  171. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  172. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  173. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  174. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  175. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  176. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  177. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  178. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  179. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  180. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  181. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  182. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  183. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  184. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  185. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  186. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  187. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  188. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  189. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  190. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  191. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  192. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  193. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  194. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  195. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  196. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  197. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  198. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  199. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  200. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  201. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  202. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  203. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  204. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  205. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  206. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  207. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  208. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  209. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  210. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  211. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  212. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  213. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  215. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  216. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  217. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  218. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  222. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  223. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  224. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  225. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  226. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  227. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  228. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  229. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  230. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  231. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  232. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  233. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  234. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  235. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  236. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  237. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  238. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  239. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  240. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  241. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  242. package/dist/extensions/pi-web-agent/package.json +2 -2
  243. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
  244. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  245. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
  246. package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
  247. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  248. package/dist/extensions/question/helpers.ts +3 -3
  249. package/dist/extensions/question/index.ts +2 -2
  250. package/dist/extensions/question/navigation.ts +2 -2
  251. package/dist/extensions/question/package.json +2 -2
  252. package/dist/extensions/question/question-list.ts +2 -2
  253. package/dist/extensions/question/tests/question-list.test.ts +2 -2
  254. package/dist/extensions/question/tui-adapter.ts +2 -2
  255. package/dist/extensions/question/types.ts +2 -2
  256. package/dist/extensions/question/ui-protocol.ts +2 -2
  257. package/dist/extensions/rtk.ts +2 -2
  258. package/dist/extensions/tokenin-onboarding.ts +2 -2
  259. package/dist/extensions/undo.ts +1 -1
  260. package/dist/extensions/web-agent-onboarding.ts +3 -3
  261. package/dist/extensions/workflow/adapter.ts +16 -8
  262. package/dist/extensions/workflow/extension.ts +1 -1
  263. package/dist/extensions/workflow/package.json +1 -1
  264. package/dist/main.d.ts.map +1 -1
  265. package/dist/main.js +4 -0
  266. package/dist/main.js.map +1 -1
  267. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/assistant-message.js +2 -0
  269. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  270. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  271. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  272. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  273. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  274. package/dist/skills/caveman/SKILL.md +49 -0
  275. package/dist/skills/handoff-text/SKILL.md +13 -0
  276. package/dist/utils/thinking-tags.d.ts +7 -0
  277. package/dist/utils/thinking-tags.d.ts.map +1 -0
  278. package/dist/utils/thinking-tags.js +63 -0
  279. package/dist/utils/thinking-tags.js.map +1 -0
  280. package/docs/docs.json +4 -0
  281. package/docs/extensions.md +2 -0
  282. package/docs/settings.md +15 -0
  283. package/docs/shared-host-extensions.md +109 -0
  284. package/package.json +2 -2
@@ -4,7 +4,8 @@ import * as path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import type { Message } from "@earendil-works/pi-ai";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
- import { consumeInterruptRequest, watchAsyncControlInbox } from "./control-channel.ts";
7
+ import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
8
+ import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
8
9
  import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
10
  import { SELESAI_PACKAGE, getSelesaiSpawnCommand, resolveInstalledSelesaiPackageRoot } from "../shared/pi-spawn.ts";
10
11
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
@@ -15,14 +16,21 @@ import {
15
16
  type AsyncParallelGroupStatus,
16
17
  type AsyncStatus,
17
18
  type ChainOutputMap,
19
+ type CostSummary,
18
20
  type ModelAttempt,
19
21
  type NestedRouteInfo,
22
+ type NestedRunSummary,
20
23
  type ResolvedControlConfig,
24
+ type ResolvedTurnBudget,
25
+ type ResolvedToolBudget,
21
26
  type SubagentRunMode,
27
+ type ToolBudgetState,
28
+ type TurnBudgetState,
22
29
  type Usage,
23
30
  type WorkflowGraphSnapshot,
24
31
  DEFAULT_MAX_OUTPUT,
25
32
  type MaxOutputConfig,
33
+ SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
26
34
  truncateOutput,
27
35
  getSubagentDepthEnv,
28
36
  } from "../../shared/types.ts";
@@ -43,15 +51,17 @@ import {
43
51
  mapConcurrent,
44
52
  aggregateParallelOutputs,
45
53
  MAX_PARALLEL_CONCURRENCY,
54
+ DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
55
+ Semaphore,
46
56
  } from "../shared/parallel-utils.ts";
47
- import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
57
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
48
58
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
49
59
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
50
60
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
51
- import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
61
+ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
52
62
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
53
63
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
54
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
64
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
55
65
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
56
66
  import {
57
67
  createMutatingFailureState,
@@ -81,7 +91,8 @@ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts
81
91
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
82
92
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
83
93
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
84
- import { createSpawnTimingMarkers, emitSpawnTiming, type SpawnTimingMarkers } from "../shared/spawn-timing.ts";
94
+ import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
95
+ import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
85
96
 
86
97
  interface SubagentRunConfig {
87
98
  id: string;
@@ -102,6 +113,7 @@ interface SubagentRunConfig {
102
113
  piArgv1?: string;
103
114
  worktreeSetupHook?: string;
104
115
  worktreeSetupHookTimeoutMs?: number;
116
+ worktreeBaseDir?: string;
105
117
  controlConfig?: ResolvedControlConfig;
106
118
  controlIntercomTarget?: string;
107
119
  childIntercomTargets?: Array<string | undefined>;
@@ -110,6 +122,12 @@ interface SubagentRunConfig {
110
122
  workflowGraph?: WorkflowGraphSnapshot;
111
123
  nestedRoute?: NestedRouteInfo;
112
124
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
125
+ timeoutMs?: number;
126
+ deadlineAt?: number;
127
+ turnBudget?: ResolvedTurnBudget;
128
+ toolBudget?: ResolvedToolBudget;
129
+ /** Global cap on simultaneously-running subagent tasks within this run. */
130
+ globalConcurrencyLimit?: number;
113
131
  }
114
132
 
115
133
  interface StepResult {
@@ -119,13 +137,23 @@ interface StepResult {
119
137
  success: boolean;
120
138
  exitCode?: number | null;
121
139
  skipped?: boolean;
140
+ interrupted?: boolean;
141
+ timedOut?: boolean;
142
+ turnBudget?: TurnBudgetState;
143
+ turnBudgetExceeded?: boolean;
144
+ wrapUpRequested?: boolean;
145
+ toolBudget?: ToolBudgetState;
146
+ toolBudgetBlocked?: boolean;
122
147
  sessionFile?: string;
123
148
  intercomTarget?: string;
124
149
  model?: string;
125
150
  attemptedModels?: string[];
126
151
  modelAttempts?: ModelAttempt[];
152
+ totalCost?: CostSummary;
127
153
  artifactPaths?: ArtifactPaths;
128
154
  truncated?: boolean;
155
+ transcriptPath?: string;
156
+ transcriptError?: string;
129
157
  structuredOutput?: unknown;
130
158
  structuredOutputPath?: string;
131
159
  structuredOutputSchemaPath?: string;
@@ -238,6 +266,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
238
266
  return total > 0 ? { input, output, total } : null;
239
267
  }
240
268
 
269
+ function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
270
+ if (!attempts || attempts.length === 0) return undefined;
271
+ let inputTokens = 0;
272
+ let outputTokens = 0;
273
+ let costUsd = 0;
274
+ for (const attempt of attempts) {
275
+ inputTokens += attempt.usage?.input ?? 0;
276
+ outputTokens += attempt.usage?.output ?? 0;
277
+ costUsd += attempt.usage?.cost ?? 0;
278
+ }
279
+ return inputTokens > 0 || outputTokens > 0 || costUsd > 0
280
+ ? { inputTokens, outputTokens, costUsd }
281
+ : undefined;
282
+ }
283
+
241
284
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
242
285
  const nonEmpty = lines.filter((line) => line.trim());
243
286
  if (nonEmpty.length === 0) return;
@@ -248,6 +291,13 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
248
291
  }
249
292
  }
250
293
 
294
+ function isTerminalAssistantStop(message: Message): boolean {
295
+ const stopReason = (message as { stopReason?: string }).stopReason;
296
+ const hasToolCall = Array.isArray(message.content)
297
+ && message.content.some((part) => (part as { type?: string }).type === "toolCall");
298
+ return stopReason === "stop" && !hasToolCall;
299
+ }
300
+
251
301
  function resetStepLiveDetail(step: RunnerStatusStep): void {
252
302
  step.currentTool = undefined;
253
303
  step.currentToolArgs = undefined;
@@ -262,7 +312,6 @@ interface ChildEventContext {
262
312
  runId: string;
263
313
  stepIndex: number;
264
314
  agent: string;
265
- markers?: SpawnTimingMarkers;
266
315
  }
267
316
 
268
317
  interface ChildUsage {
@@ -297,6 +346,12 @@ interface RunPiStreamingResult {
297
346
  error?: string;
298
347
  finalOutput: string;
299
348
  interrupted?: boolean;
349
+ timedOut?: boolean;
350
+ turnBudget?: TurnBudgetState;
351
+ turnBudgetExceeded?: boolean;
352
+ wrapUpRequested?: boolean;
353
+ toolBudget?: ToolBudgetState;
354
+ toolBudgetBlocked?: boolean;
300
355
  observedMutationAttempt?: boolean;
301
356
  }
302
357
 
@@ -311,21 +366,18 @@ function runPiStreaming(
311
366
  childEventContext?: ChildEventContext,
312
367
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
313
368
  onChildEvent?: (event: ChildEvent) => void,
369
+ transcriptWriter?: ChildTranscriptWriter,
370
+ registerTimeout?: (interrupt: (() => void) | undefined) => void,
371
+ timeoutMessage?: string,
372
+ registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
314
373
  ): Promise<RunPiStreamingResult> {
315
374
  return new Promise((resolve) => {
316
375
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
317
376
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
318
- const markers = childEventContext ? createSpawnTimingMarkers() : undefined;
319
- if (markers) {
320
- emitSpawnTiming({ eventsPath: childEventContext.eventsPath, marker: "spawn_start", markers, runId: childEventContext.runId, stepIndex: childEventContext.stepIndex, agent: childEventContext.agent });
321
- }
322
377
  const spawnSpec = getSelesaiSpawnCommand(args, {
323
378
  ...(piPackageRoot ? { piPackageRoot } : {}),
324
379
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
325
380
  });
326
- if (markers) {
327
- emitSpawnTiming({ eventsPath: childEventContext.eventsPath, marker: "spawn_command_resolved", markers, runId: childEventContext.runId, stepIndex: childEventContext.stepIndex, agent: childEventContext.agent });
328
- }
329
381
  const child = spawn(spawnSpec.command, spawnSpec.args, {
330
382
  cwd,
331
383
  stdio: ["ignore", "pipe", "pipe"],
@@ -341,23 +393,13 @@ function runPiStreaming(
341
393
  let error: string | undefined;
342
394
  let assistantError: string | undefined;
343
395
  let interrupted = false;
396
+ let timedOut = false;
397
+ let turnBudgetExceeded = false;
398
+ let turnBudgetMessage: string | undefined;
399
+ let turnBudget: TurnBudgetState | undefined;
344
400
  let observedMutationAttempt = false;
345
- let firstEventEmitted = false;
346
- let firstJsonlEventEmitted = false;
347
401
  const rawStdoutLines: string[] = [];
348
402
 
349
- const emitChildTiming = (marker: import("../shared/spawn-timing.ts").SpawnTimingMarker) => {
350
- if (!markers || !childEventContext) return;
351
- emitSpawnTiming({
352
- eventsPath: childEventContext.eventsPath,
353
- marker,
354
- markers,
355
- runId: childEventContext.runId,
356
- stepIndex: childEventContext.stepIndex,
357
- agent: childEventContext.agent,
358
- });
359
- };
360
-
361
403
  const writeOutputLine = (line: string) => {
362
404
  if (!line.trim()) return;
363
405
  outputStream.write(`${line}\n`);
@@ -384,6 +426,8 @@ function runPiStreaming(
384
426
 
385
427
  const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
386
428
  appendChildEvent({ type, line });
429
+ if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
430
+ else transcriptWriter?.writeStderrLine(line);
387
431
  };
388
432
 
389
433
  const processStdoutLine = (line: string) => {
@@ -391,10 +435,6 @@ function runPiStreaming(
391
435
  let event: ChildEvent;
392
436
  try {
393
437
  event = JSON.parse(line) as ChildEvent;
394
- if (!firstJsonlEventEmitted) {
395
- firstJsonlEventEmitted = true;
396
- emitChildTiming("child_first_jsonl_event");
397
- }
398
438
  } catch {
399
439
  rawStdoutLines.push(line);
400
440
  writeOutputLine(line);
@@ -403,6 +443,7 @@ function runPiStreaming(
403
443
  }
404
444
 
405
445
  appendChildEvent(event);
446
+ transcriptWriter?.writeChildEvent(event);
406
447
  onChildEvent?.(event);
407
448
 
408
449
  if (event.type === "tool_execution_start" && event.toolName) {
@@ -429,10 +470,7 @@ function runPiStreaming(
429
470
  usage.cacheWrite += eventUsage.cacheWrite ?? 0;
430
471
  usage.cost += eventUsage.cost?.total ?? 0;
431
472
  }
432
- const stopReason = (event.message as { stopReason?: string }).stopReason;
433
- const hasToolCall = Array.isArray(event.message.content)
434
- && event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
435
- if (stopReason === "stop" && !hasToolCall) {
473
+ if (isTerminalAssistantStop(event.message)) {
436
474
  if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
437
475
  cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
438
476
  startFinalDrain();
@@ -457,18 +495,18 @@ function runPiStreaming(
457
495
  // a lingering stdio holder after `exit`, or a child that never exits.
458
496
  const FINAL_STOP_GRACE_MS = 1000;
459
497
  const HARD_KILL_MS = 3000;
498
+ const TIMEOUT_HARD_KILL_MS = 3000;
460
499
  let childExited = false;
461
500
  let forcedTerminationSignal = false;
462
501
  let cleanTerminalAssistantStopReceived = false;
463
502
  let finalDrainTimer: NodeJS.Timeout | undefined;
464
503
  let finalHardKillTimer: NodeJS.Timeout | undefined;
504
+ let timeoutHardKillTimer: NodeJS.Timeout | undefined;
505
+ let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
506
+ let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
465
507
  let settled = false;
466
508
  const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
467
509
  child.stdout.on("data", (chunk: Buffer) => {
468
- if (!firstEventEmitted) {
469
- firstEventEmitted = true;
470
- emitChildTiming("child_first_event");
471
- }
472
510
  const text = chunk.toString();
473
511
  stdoutBuf += text;
474
512
  const lines = stdoutBuf.split("\n");
@@ -477,21 +515,45 @@ function runPiStreaming(
477
515
  });
478
516
 
479
517
  child.stderr.on("data", (chunk: Buffer) => {
480
- if (!firstEventEmitted) {
481
- firstEventEmitted = true;
482
- emitChildTiming("child_first_event");
483
- }
484
518
  processStderrText(chunk.toString());
485
519
  });
486
520
  registerInterrupt?.(() => {
487
- if (settled) return;
521
+ if (settled || timedOut) return;
488
522
  interrupted = true;
489
523
  if (!error) error = "Interrupted. Waiting for explicit next action.";
490
524
  trySignalChild(child, "SIGINT");
491
525
  setTimeout(() => {
492
- if (!settled) trySignalChild(child, "SIGTERM");
526
+ if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
493
527
  }, 1000).unref?.();
494
528
  });
529
+ registerTimeout?.(() => {
530
+ if (settled || timedOut) return;
531
+ timedOut = true;
532
+ interrupted = false;
533
+ error = timeoutMessage ?? "Subagent timed out.";
534
+ trySignalChild(child, "SIGTERM");
535
+ timeoutHardKillTimer = setTimeout(() => {
536
+ if (!settled) trySignalChild(child, "SIGKILL");
537
+ }, TIMEOUT_HARD_KILL_MS);
538
+ timeoutHardKillTimer.unref?.();
539
+ });
540
+ registerTurnBudgetAbort?.((message, state) => {
541
+ if (settled || timedOut || turnBudgetExceeded) return;
542
+ turnBudgetExceeded = true;
543
+ turnBudgetMessage = message;
544
+ turnBudget = state;
545
+ interrupted = false;
546
+ error = message;
547
+ trySignalChild(child, "SIGINT");
548
+ turnBudgetTerminationTimer = setTimeout(() => {
549
+ if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
550
+ }, 1000);
551
+ turnBudgetTerminationTimer.unref?.();
552
+ turnBudgetHardKillTimer = setTimeout(() => {
553
+ if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
554
+ }, 4000);
555
+ turnBudgetHardKillTimer.unref?.();
556
+ });
495
557
  const clearDrainTimers = () => {
496
558
  if (finalDrainTimer) {
497
559
  clearTimeout(finalDrainTimer);
@@ -501,6 +563,18 @@ function runPiStreaming(
501
563
  clearTimeout(finalHardKillTimer);
502
564
  finalHardKillTimer = undefined;
503
565
  }
566
+ if (timeoutHardKillTimer) {
567
+ clearTimeout(timeoutHardKillTimer);
568
+ timeoutHardKillTimer = undefined;
569
+ }
570
+ if (turnBudgetTerminationTimer) {
571
+ clearTimeout(turnBudgetTerminationTimer);
572
+ turnBudgetTerminationTimer = undefined;
573
+ }
574
+ if (turnBudgetHardKillTimer) {
575
+ clearTimeout(turnBudgetHardKillTimer);
576
+ turnBudgetHardKillTimer = undefined;
577
+ }
504
578
  };
505
579
  function startFinalDrain(): void {
506
580
  if (childExited || finalDrainTimer || settled) return;
@@ -527,6 +601,8 @@ function runPiStreaming(
527
601
  child.on("close", (exitCode, signal) => {
528
602
  settled = true;
529
603
  registerInterrupt?.(undefined);
604
+ registerTimeout?.(undefined);
605
+ registerTurnBudgetAbort?.(undefined);
530
606
  clearDrainTimers();
531
607
  clearStdioGuard();
532
608
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
@@ -537,13 +613,17 @@ function runPiStreaming(
537
613
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
538
614
  resolve({
539
615
  stderr,
540
- exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
616
+ exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
541
617
  messages,
542
618
  usage,
543
619
  model,
544
- error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
545
- finalOutput,
620
+ error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
621
+ finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
546
622
  interrupted,
623
+ timedOut,
624
+ turnBudget,
625
+ turnBudgetExceeded,
626
+ wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
547
627
  observedMutationAttempt,
548
628
  });
549
629
  });
@@ -551,12 +631,14 @@ function runPiStreaming(
551
631
  child.on("error", (spawnError) => {
552
632
  settled = true;
553
633
  registerInterrupt?.(undefined);
634
+ registerTimeout?.(undefined);
635
+ registerTurnBudgetAbort?.(undefined);
554
636
  clearDrainTimers();
555
637
  clearStdioGuard();
556
638
  outputStream.end();
557
639
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
558
640
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
559
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
641
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
560
642
  });
561
643
  });
562
644
  }
@@ -681,14 +763,22 @@ interface SingleStepContext {
681
763
  flatIndex: number;
682
764
  flatStepCount: number;
683
765
  outputFile: string;
766
+ steerInboxDir?: string;
767
+ transcriptPath?: string;
684
768
  piPackageRoot?: string;
685
769
  piArgv1?: string;
686
770
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
771
+ registerTimeout?: (interrupt: (() => void) | undefined) => void;
772
+ registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
773
+ timeoutSignal?: AbortSignal;
774
+ timeoutMessage?: string;
775
+ turnBudget?: ResolvedTurnBudget;
687
776
  childIntercomTarget?: string;
688
777
  orchestratorIntercomTarget?: string;
689
778
  nestedRoute?: NestedRouteInfo;
690
779
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
691
780
  onChildEvent?: (event: ChildEvent) => void;
781
+ skipAcceptance?: () => boolean;
692
782
  }
693
783
 
694
784
  /** Run a single pi agent step, returning output and metadata */
@@ -704,7 +794,15 @@ async function runSingleStep(
704
794
  attemptedModels?: string[];
705
795
  modelAttempts?: ModelAttempt[];
706
796
  artifactPaths?: ArtifactPaths;
797
+ transcriptPath?: string;
798
+ transcriptError?: string;
707
799
  interrupted?: boolean;
800
+ timedOut?: boolean;
801
+ turnBudget?: TurnBudgetState;
802
+ turnBudgetExceeded?: boolean;
803
+ wrapUpRequested?: boolean;
804
+ toolBudget?: ToolBudgetState;
805
+ toolBudgetBlocked?: boolean;
708
806
  sessionFile?: string;
709
807
  intercomTarget?: string;
710
808
  completionGuardTriggered?: boolean;
@@ -714,27 +812,52 @@ async function runSingleStep(
714
812
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
715
813
  }> {
716
814
  if (step.importAsyncRoot) {
717
- const imported = await waitForImportedAsyncRoot(step.importAsyncRoot);
815
+ let importTimedOut = false;
816
+ ctx.registerTimeout?.(() => {
817
+ importTimedOut = true;
818
+ let pid: number | undefined;
819
+ try {
820
+ pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
821
+ } catch {
822
+ pid = undefined;
823
+ }
824
+ try {
825
+ deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
826
+ } catch {
827
+ // The parent runner's own timeout result is authoritative for the attached step.
828
+ }
829
+ });
718
830
  try {
719
- fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
720
- } catch {
721
- // Output files are observability only for imported roots.
831
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
832
+ shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
833
+ timeoutMessage: ctx.timeoutMessage,
834
+ });
835
+ try {
836
+ fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
837
+ } catch {
838
+ // Output files are observability only for imported roots.
839
+ }
840
+ const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
841
+ return {
842
+ agent: imported.agent,
843
+ output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
844
+ exitCode: timedOut ? 1 : imported.exitCode,
845
+ error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
846
+ timedOut: timedOut ? true : undefined,
847
+ sessionFile: imported.sessionFile,
848
+ intercomTarget: imported.intercomTarget,
849
+ model: imported.model,
850
+ attemptedModels: imported.attemptedModels,
851
+ modelAttempts: imported.modelAttempts,
852
+ totalCost: imported.totalCost,
853
+ structuredOutput: timedOut ? undefined : imported.structuredOutput,
854
+ structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
855
+ structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
856
+ acceptance: timedOut ? undefined : imported.acceptance,
857
+ };
858
+ } finally {
859
+ ctx.registerTimeout?.(undefined);
722
860
  }
723
- return {
724
- agent: imported.agent,
725
- output: imported.output,
726
- exitCode: imported.exitCode,
727
- error: imported.error,
728
- sessionFile: imported.sessionFile,
729
- intercomTarget: imported.intercomTarget,
730
- model: imported.model,
731
- attemptedModels: imported.attemptedModels,
732
- modelAttempts: imported.modelAttempts,
733
- structuredOutput: imported.structuredOutput,
734
- structuredOutputPath: imported.structuredOutputPath,
735
- structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
736
- acceptance: imported.acceptance,
737
- };
738
861
  }
739
862
 
740
863
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
@@ -752,6 +875,7 @@ async function runSingleStep(
752
875
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
753
876
 
754
877
  let artifactPaths: ArtifactPaths | undefined;
878
+ let transcriptWriter: ChildTranscriptWriter | undefined;
755
879
  if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
756
880
  const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
757
881
  artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
@@ -759,7 +883,18 @@ async function runSingleStep(
759
883
  if (ctx.artifactConfig?.includeInput !== false) {
760
884
  fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
761
885
  }
886
+ if (ctx.artifactConfig?.includeTranscript !== false) {
887
+ transcriptWriter = createChildTranscriptWriter({
888
+ transcriptPath: artifactPaths.transcriptPath,
889
+ source: "async",
890
+ runId: ctx.id,
891
+ agent: step.agent,
892
+ childIndex: ctx.flatIndex,
893
+ cwd: step.cwd ?? ctx.cwd,
894
+ });
895
+ }
762
896
  }
897
+ transcriptWriter?.writeInitialUserMessage(task);
763
898
 
764
899
  const candidates = step.modelCandidates && step.modelCandidates.length > 0
765
900
  ? step.modelCandidates
@@ -773,8 +908,12 @@ async function runSingleStep(
773
908
  let finalResult: RunPiStreamingResult | undefined;
774
909
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
775
910
  let completionGuardTriggeredFinal = false;
911
+ let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
912
+ let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
913
+ let toolBudgetBlocked = false;
776
914
 
777
915
  for (let index = 0; index < candidates.length; index++) {
916
+ if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
778
917
  const candidate = candidates[index];
779
918
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
780
919
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
@@ -799,7 +938,7 @@ async function runSingleStep(
799
938
  tools: step.tools,
800
939
  extensions: step.extensions,
801
940
  subagentOnlyExtensions: step.subagentOnlyExtensions,
802
- systemPrompt: step.systemPrompt,
941
+ systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
803
942
  systemPromptMode: step.systemPromptMode,
804
943
  mcpDirectTools: step.mcpDirectTools,
805
944
  cwd: step.cwd ?? ctx.cwd,
@@ -813,7 +952,9 @@ async function runSingleStep(
813
952
  parentControlInbox: ctx.nestedRoute?.controlInbox,
814
953
  parentRootRunId: ctx.nestedRoute?.rootRunId,
815
954
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
955
+ steerInboxDir: ctx.steerInboxDir,
816
956
  structuredOutput: effectiveStructuredOutput,
957
+ toolBudget: step.toolBudget,
817
958
  });
818
959
  const run = await runPiStreaming(
819
960
  args,
@@ -826,13 +967,38 @@ async function runSingleStep(
826
967
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
827
968
  ctx.registerInterrupt,
828
969
  ctx.onChildEvent,
970
+ transcriptWriter,
971
+ ctx.registerTimeout,
972
+ ctx.timeoutMessage,
973
+ ctx.registerTurnBudgetAbort,
829
974
  );
975
+ if (run.turnBudget) turnBudget = run.turnBudget;
976
+ else if (ctx.turnBudget) {
977
+ const assistantMessages = run.messages.filter((message) => message.role === "assistant");
978
+ const turnCount = assistantMessages.length;
979
+ const lastAssistantMessage = assistantMessages.at(-1);
980
+ if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
981
+ turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
982
+ } else if (turnCount >= ctx.turnBudget.maxTurns) {
983
+ turnBudget = turnBudgetState(
984
+ ctx.turnBudget,
985
+ turnCount,
986
+ shouldAbortForTurnBudget(ctx.turnBudget, turnCount, lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false),
987
+ );
988
+ }
989
+ }
830
990
  cleanupTempDir(tempDir);
831
991
 
832
992
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
993
+ const missingStructuredOutput = effectiveStructuredOutput
994
+ ? !fs.existsSync(effectiveStructuredOutput.outputPath)
995
+ : false;
996
+ const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
997
+ ? "Subagent produced no output (possible model cold-start or empty response)."
998
+ : undefined;
833
999
  let structuredOutput: unknown;
834
1000
  let structuredError: string | undefined;
835
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
1001
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
836
1002
  const structured = readStructuredOutput({
837
1003
  schema: effectiveStructuredOutput.schema,
838
1004
  schemaPath: effectiveStructuredOutput.schemaPath,
@@ -841,7 +1007,7 @@ async function runSingleStep(
841
1007
  if (structured.error) structuredError = structured.error;
842
1008
  else structuredOutput = structured.value;
843
1009
  }
844
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
1010
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
845
1011
  ? evaluateCompletionMutationGuard({
846
1012
  agent: step.agent,
847
1013
  task: taskForCompletionGuard,
@@ -860,16 +1026,18 @@ async function runSingleStep(
860
1026
  ? 1
861
1027
  : hiddenError?.hasError
862
1028
  ? (hiddenError.exitCode ?? 1)
863
- : run.error && run.exitCode === 0
1029
+ : emptyOutputError
864
1030
  ? 1
865
- : run.exitCode;
1031
+ : run.error && run.exitCode === 0
1032
+ ? 1
1033
+ : run.exitCode;
866
1034
  const error = completionGuardError
867
1035
  ?? structuredError
868
1036
  ?? (hiddenError?.hasError
869
1037
  ? hiddenError.details
870
1038
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
871
1039
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
872
- : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1040
+ : emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
873
1041
  const attempt: ModelAttempt = {
874
1042
  model: candidate ?? run.model ?? step.model ?? "default",
875
1043
  success: effectiveExitCode === 0 && !error,
@@ -881,7 +1049,15 @@ async function runSingleStep(
881
1049
  if (candidate) attemptedModels.push(candidate);
882
1050
  completionGuardTriggeredFinal = completionGuardTriggered;
883
1051
  finalOutputSnapshot = outputSnapshot;
1052
+ if (step.toolBudget) {
1053
+ const toolMessages = run.messages.filter((message) => message.role === "toolResult");
1054
+ const blockedMessage = toolMessages.find((message) => extractTextFromContent(message.content).includes("Tool budget hard limit reached"));
1055
+ toolBudgetBlocked = Boolean(blockedMessage);
1056
+ toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1057
+ }
884
1058
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
1059
+ if (run.turnBudgetExceeded) break;
1060
+ if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
885
1061
  if (attempt.success || completionGuardTriggered) break;
886
1062
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
887
1063
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
@@ -898,6 +1074,12 @@ async function runSingleStep(
898
1074
  if (attemptNotes.length > 0) {
899
1075
  outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
900
1076
  }
1077
+ if (!finalResult?.timedOut && finalResult?.turnBudgetExceeded && turnBudget) {
1078
+ outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
1079
+ } else if (!finalResult?.timedOut && turnBudget?.outcome === "wrap-up-requested") {
1080
+ const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
1081
+ outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
1082
+ }
901
1083
  const outputForAcceptance = rawOutput;
902
1084
  const finalizedOutput = finalizeSingleOutput({
903
1085
  fullOutput: outputForSummary,
@@ -909,19 +1091,28 @@ async function runSingleStep(
909
1091
  saveError: resolvedOutput.saveError,
910
1092
  });
911
1093
  outputForSummary = finalizedOutput.displayOutput;
912
- const acceptance = step.effectiveAcceptance
1094
+ const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
913
1095
  ? await evaluateAcceptance({
914
1096
  acceptance: step.effectiveAcceptance,
915
1097
  output: outputForAcceptance,
916
1098
  cwd: step.cwd ?? ctx.cwd,
1099
+ signal: ctx.timeoutSignal,
1100
+ abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
917
1101
  })
918
1102
  : undefined;
919
- const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
920
- const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
921
- const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
922
- const effectiveFinalError = acceptanceCanFailRun
923
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
924
- : finalResult?.error;
1103
+ const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
1104
+ const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
1105
+ const effectiveAcceptance = timedOutAfterAcceptance || turnBudgetExceeded ? undefined : acceptance;
1106
+ const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1107
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
1108
+ const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1109
+ const effectiveFinalError = timedOutAfterAcceptance
1110
+ ? ctx.timeoutMessage ?? "Subagent timed out."
1111
+ : turnBudgetExceeded
1112
+ ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1113
+ : acceptanceCanFailRun
1114
+ ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1115
+ : finalResult?.error;
925
1116
 
926
1117
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
927
1118
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -938,6 +1129,8 @@ async function runSingleStep(
938
1129
  model: finalResult?.model,
939
1130
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
940
1131
  modelAttempts,
1132
+ ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1133
+ transcriptError: transcriptWriter?.getError(),
941
1134
  skills: step.skills,
942
1135
  timestamp: Date.now(),
943
1136
  }, null, 2),
@@ -956,13 +1149,22 @@ async function runSingleStep(
956
1149
  model: finalResult?.model,
957
1150
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
958
1151
  modelAttempts,
1152
+ totalCost: costSummaryFromAttempts(modelAttempts),
959
1153
  artifactPaths,
960
- interrupted: finalResult?.interrupted,
1154
+ transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1155
+ transcriptError: transcriptWriter?.getError(),
1156
+ interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
1157
+ timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
1158
+ turnBudget,
1159
+ turnBudgetExceeded: turnBudgetExceeded || undefined,
1160
+ wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
1161
+ toolBudget,
1162
+ toolBudgetBlocked: toolBudgetBlocked || undefined,
961
1163
  completionGuardTriggered: completionGuardTriggeredFinal,
962
- structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
963
- structuredOutputPath: effectiveStructuredOutput?.outputPath,
964
- structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
965
- acceptance,
1164
+ structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1165
+ structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1166
+ structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1167
+ acceptance: effectiveAcceptance,
966
1168
  };
967
1169
  }
968
1170
 
@@ -1091,9 +1293,29 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
1091
1293
  writeInitialProgressFile(cwd);
1092
1294
  }
1093
1295
 
1094
- export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1296
+ function resolveAsyncStepTranscriptPath(input: {
1297
+ artifactsDir?: string;
1298
+ artifactConfig?: Partial<ArtifactConfig>;
1299
+ runId: string;
1300
+ agent: string;
1301
+ flatIndex: number;
1302
+ flatStepCount: number;
1303
+ }): string | undefined {
1304
+ if (!input.artifactsDir || input.artifactConfig?.enabled === false || input.artifactConfig?.includeTranscript === false) return undefined;
1305
+ return getArtifactPaths(
1306
+ input.artifactsDir,
1307
+ input.runId,
1308
+ input.agent,
1309
+ input.flatStepCount > 1 ? input.flatIndex : undefined,
1310
+ ).transcriptPath;
1311
+ }
1312
+
1313
+ type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
1314
+
1315
+ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1095
1316
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1096
1317
  config;
1318
+ const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1097
1319
  let previousOutput = "";
1098
1320
  const outputs: ChainOutputMap = {};
1099
1321
  const results: StepResult[] = [];
@@ -1104,13 +1326,23 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1104
1326
  const eventsPath = path.join(asyncDir, "events.jsonl");
1105
1327
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
1106
1328
  const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
1107
- let activeChildInterrupt: (() => void) | undefined;
1329
+ const activeChildInterrupts = new Map<number, () => void>();
1330
+ const activeChildTimeouts = new Map<number, () => void>();
1331
+ const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
1332
+ const pendingStepSteers: SteerRequest[] = [];
1108
1333
  let interrupted = false;
1109
1334
  let currentActivityState: ActivityState | undefined;
1110
1335
  let activityTimer: NodeJS.Timeout | undefined;
1336
+ let timeoutTimer: NodeJS.Timeout | undefined;
1337
+ let timedOut = false;
1338
+ let turnBudgetExceeded = false;
1339
+ const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
1340
+ const timeoutAbortController = new AbortController();
1111
1341
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1112
1342
  let latestSessionFile: string | undefined;
1113
1343
 
1344
+ const flatSteps = flattenSteps(steps);
1345
+ const initialFlatStepCount = flatSteps.length;
1114
1346
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
1115
1347
  const initialStatusSteps: RunnerStatusStep[] = [];
1116
1348
  let flatStepCount = 0;
@@ -1119,6 +1351,8 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1119
1351
  if (isParallelGroup(step)) {
1120
1352
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1121
1353
  for (const task of step.parallel) {
1354
+ const taskFlatIndex = flatStepCount;
1355
+ const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1122
1356
  initialStatusSteps.push({
1123
1357
  agent: task.agent,
1124
1358
  phase: task.phase,
@@ -1126,7 +1360,9 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1126
1360
  outputName: task.outputName,
1127
1361
  structured: task.structured,
1128
1362
  status: "pending",
1363
+ ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1129
1364
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1365
+ ...(transcriptPath ? { transcriptPath } : {}),
1130
1366
  skills: task.skills,
1131
1367
  model: task.model,
1132
1368
  thinking: task.thinking,
@@ -1134,8 +1370,8 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1134
1370
  recentTools: [],
1135
1371
  recentOutput: [],
1136
1372
  });
1373
+ flatStepCount++;
1137
1374
  }
1138
- flatStepCount += step.parallel.length;
1139
1375
  } else if (isDynamicRunnerGroup(step)) {
1140
1376
  parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1141
1377
  initialStatusSteps.push({
@@ -1145,11 +1381,14 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1145
1381
  outputName: step.collect.as,
1146
1382
  structured: Boolean(step.collect.outputSchema),
1147
1383
  status: "pending",
1384
+ ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1148
1385
  recentTools: [],
1149
1386
  recentOutput: [],
1150
1387
  });
1151
1388
  flatStepCount++;
1152
1389
  } else {
1390
+ const stepFlatIndex = flatStepCount;
1391
+ const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1153
1392
  initialStatusSteps.push({
1154
1393
  agent: step.agent,
1155
1394
  phase: step.phase,
@@ -1157,7 +1396,9 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1157
1396
  outputName: step.outputName,
1158
1397
  structured: step.structured,
1159
1398
  status: "pending",
1399
+ ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1160
1400
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
1401
+ ...(transcriptPath ? { transcriptPath } : {}),
1161
1402
  skills: step.skills,
1162
1403
  model: step.model,
1163
1404
  thinking: step.thinking,
@@ -1168,11 +1409,11 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1168
1409
  flatStepCount++;
1169
1410
  }
1170
1411
  }
1171
- const flatSteps = flattenSteps(steps);
1172
1412
  const sessionEnabled = Boolean(config.sessionDir)
1173
1413
  || shareEnabled
1174
1414
  || flatSteps.some((step) => Boolean(step.sessionFile));
1175
1415
  const statusPayload: RunnerStatusPayload = {
1416
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1176
1417
  runId: id,
1177
1418
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1178
1419
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
@@ -1180,6 +1421,10 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1180
1421
  lastActivityAt: overallStartTime,
1181
1422
  startedAt: overallStartTime,
1182
1423
  lastUpdate: overallStartTime,
1424
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
1425
+ ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1426
+ ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
1427
+ ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
1183
1428
  pid: process.pid,
1184
1429
  cwd,
1185
1430
  currentStep: 0,
@@ -1251,6 +1496,117 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1251
1496
  writeAtomicJson(statusPath, statusPayload);
1252
1497
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1253
1498
  };
1499
+ const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1500
+ if (!interrupt) {
1501
+ activeChildInterrupts.delete(flatIndex);
1502
+ return;
1503
+ }
1504
+ activeChildInterrupts.set(flatIndex, interrupt);
1505
+ if (interrupted) interrupt();
1506
+ };
1507
+ const registerStepTimeout = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1508
+ if (!interrupt) {
1509
+ activeChildTimeouts.delete(flatIndex);
1510
+ return;
1511
+ }
1512
+ activeChildTimeouts.set(flatIndex, interrupt);
1513
+ if (timedOut) interrupt();
1514
+ };
1515
+ const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
1516
+ if (!abort) {
1517
+ activeChildTurnBudgetAborts.delete(flatIndex);
1518
+ return;
1519
+ }
1520
+ activeChildTurnBudgetAborts.set(flatIndex, abort);
1521
+ };
1522
+ const interruptActiveChildren = (): void => {
1523
+ for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
1524
+ };
1525
+ const timeoutActiveChildren = (): void => {
1526
+ for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
1527
+ };
1528
+ const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
1529
+ for (const child of children ?? []) {
1530
+ yield child;
1531
+ yield* nestedRuns(child.children);
1532
+ yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
1533
+ }
1534
+ };
1535
+ const interruptNestedAsyncDescendants = (): void => {
1536
+ if (!config.nestedRoute) return;
1537
+ let registry: ReturnType<typeof projectNestedEvents>;
1538
+ try {
1539
+ registry = projectNestedEvents(config.nestedRoute);
1540
+ } catch (error) {
1541
+ appendJsonl(eventsPath, JSON.stringify({
1542
+ type: "subagent.nested.interrupt_failed",
1543
+ ts: Date.now(),
1544
+ runId: id,
1545
+ message: error instanceof Error ? error.message : String(error),
1546
+ }));
1547
+ return;
1548
+ }
1549
+ for (const run of nestedRuns(registry.children)) {
1550
+ if (run.state !== "running" && run.state !== "queued") continue;
1551
+ const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1552
+ if (!nestedAsyncDir) continue;
1553
+ try {
1554
+ deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
1555
+ } catch (error) {
1556
+ appendJsonl(eventsPath, JSON.stringify({
1557
+ type: "subagent.nested.interrupt_failed",
1558
+ ts: Date.now(),
1559
+ runId: id,
1560
+ targetRunId: run.id,
1561
+ message: error instanceof Error ? error.message : String(error),
1562
+ }));
1563
+ }
1564
+ }
1565
+ };
1566
+ const timeoutNestedAsyncDescendants = (): void => {
1567
+ if (!config.nestedRoute) return;
1568
+ let registry: ReturnType<typeof projectNestedEvents>;
1569
+ try {
1570
+ registry = projectNestedEvents(config.nestedRoute);
1571
+ } catch (error) {
1572
+ appendJsonl(eventsPath, JSON.stringify({
1573
+ type: "subagent.nested.timeout_failed",
1574
+ ts: Date.now(),
1575
+ runId: id,
1576
+ message: error instanceof Error ? error.message : String(error),
1577
+ }));
1578
+ return;
1579
+ }
1580
+ for (const run of nestedRuns(registry.children)) {
1581
+ if (run.state !== "running" && run.state !== "queued") continue;
1582
+ const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1583
+ if (!nestedAsyncDir) continue;
1584
+ try {
1585
+ deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
1586
+ } catch (error) {
1587
+ appendJsonl(eventsPath, JSON.stringify({
1588
+ type: "subagent.nested.timeout_failed",
1589
+ ts: Date.now(),
1590
+ runId: id,
1591
+ targetRunId: run.id,
1592
+ message: error instanceof Error ? error.message : String(error),
1593
+ }));
1594
+ }
1595
+ }
1596
+ };
1597
+ const pausedStepResult = (agent: string): SingleStepResult => ({
1598
+ agent,
1599
+ output: "Paused after interrupt. Waiting for explicit next action.",
1600
+ exitCode: 0,
1601
+ interrupted: true,
1602
+ });
1603
+ const timedOutStepResult = (agent: string): SingleStepResult => ({
1604
+ agent,
1605
+ output: timeoutMessage ?? "Subagent timed out.",
1606
+ error: timeoutMessage ?? "Subagent timed out.",
1607
+ exitCode: 1,
1608
+ timedOut: true,
1609
+ });
1254
1610
  const consumePendingAppendRequests = (): void => {
1255
1611
  if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1256
1612
  const requests = consumeChainAppendRequests(asyncDir);
@@ -1381,6 +1737,58 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1381
1737
  appendControlEvent(event);
1382
1738
  return true;
1383
1739
  };
1740
+ const deliverSteerRequest = (request: SteerRequest): void => {
1741
+ if (statusPayload.state !== "running") return;
1742
+ const runningIndexes = statusPayload.steps
1743
+ .map((step, index) => ({ step, index }))
1744
+ .filter(({ step }) => step.status === "running")
1745
+ .map(({ index }) => index);
1746
+ const targets = request.targetIndex !== undefined ? [request.targetIndex] : runningIndexes;
1747
+ const now = Date.now();
1748
+ const accepted: number[] = [];
1749
+ const rejected: Array<{ index: number; reason: string }> = [];
1750
+ for (const index of targets) {
1751
+ const step = statusPayload.steps[index];
1752
+ if (!step) {
1753
+ rejected.push({ index, reason: "child index out of range" });
1754
+ continue;
1755
+ }
1756
+ if (step.status !== "running") {
1757
+ rejected.push({ index, reason: `child is ${step.status}` });
1758
+ continue;
1759
+ }
1760
+ enqueueStepSteer(asyncDir, index, request);
1761
+ step.steerCount = (step.steerCount ?? 0) + 1;
1762
+ step.lastSteerAt = now;
1763
+ accepted.push(index);
1764
+ }
1765
+ if (accepted.length > 0) {
1766
+ statusPayload.steerCount = (statusPayload.steerCount ?? 0) + accepted.length;
1767
+ statusPayload.lastSteerAt = now;
1768
+ statusPayload.lastUpdate = now;
1769
+ writeStatusPayload();
1770
+ }
1771
+ appendJsonl(eventsPath, JSON.stringify({
1772
+ type: "subagent.steer.requested",
1773
+ ts: now,
1774
+ runId: id,
1775
+ requestId: request.id,
1776
+ message: request.message,
1777
+ ...(request.source ? { source: request.source } : {}),
1778
+ ...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
1779
+ acceptedIndexes: accepted,
1780
+ ...(rejected.length ? { rejected } : {}),
1781
+ }));
1782
+ };
1783
+ const flushPendingStepSteers = (flatIndex: number): void => {
1784
+ const remaining: SteerRequest[] = [];
1785
+ for (const request of pendingStepSteers.splice(0)) {
1786
+ if (request.targetIndex === undefined) deliverSteerRequest({ ...request, targetIndex: flatIndex });
1787
+ else if (request.targetIndex === flatIndex) deliverSteerRequest(request);
1788
+ else remaining.push(request);
1789
+ }
1790
+ pendingStepSteers.push(...remaining);
1791
+ };
1384
1792
  const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
1385
1793
  const step = statusPayload.steps[flatIndex];
1386
1794
  if (!step) return;
@@ -1389,6 +1797,40 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1389
1797
  statusPayload.lastUpdate = now;
1390
1798
  writeStatusPayload();
1391
1799
  };
1800
+ const updateStepTurnBudget = (flatIndex: number, turnCount: number, now: number, terminalAssistantStop: boolean): void => {
1801
+ const budget = config.turnBudget;
1802
+ const step = statusPayload.steps[flatIndex];
1803
+ if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
1804
+ if (turnCount < budget.maxTurns) {
1805
+ const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
1806
+ step.turnBudget = state;
1807
+ statusPayload.turnBudget = state;
1808
+ return;
1809
+ }
1810
+ const state = turnBudgetState(budget, turnCount, false);
1811
+ step.turnBudget = state;
1812
+ statusPayload.turnBudget = state;
1813
+ if (!step.wrapUpRequested) {
1814
+ step.wrapUpRequested = true;
1815
+ statusPayload.wrapUpRequested = true;
1816
+ appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
1817
+ }
1818
+ if (!shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) return;
1819
+ const exceededState = turnBudgetState(budget, turnCount, true);
1820
+ const message = turnBudgetExceededMessage(budget, turnCount);
1821
+ step.turnBudget = exceededState;
1822
+ step.turnBudgetExceeded = true;
1823
+ step.wrapUpRequested = true;
1824
+ step.error = message;
1825
+ turnBudgetExceeded = true;
1826
+ statusPayload.turnBudget = exceededState;
1827
+ statusPayload.turnBudgetExceeded = true;
1828
+ statusPayload.wrapUpRequested = true;
1829
+ statusPayload.error = message;
1830
+ statusPayload.lastUpdate = now;
1831
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.turn_budget_exceeded", ts: now, runId: id, stepIndex: flatIndex, agent: step.agent, turnCount, maxTurns: budget.maxTurns, graceTurns: budget.graceTurns, message }));
1832
+ activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
1833
+ };
1392
1834
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
1393
1835
  const step = statusPayload.steps[flatIndex];
1394
1836
  if (!step) return;
@@ -1398,6 +1840,11 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1398
1840
  const mutates = isMutatingTool(event.toolName, event.args);
1399
1841
  const currentPath = resolveCurrentPath(event.toolName, event.args);
1400
1842
  step.toolCount = (step.toolCount ?? 0) + 1;
1843
+ const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
1844
+ if (configuredToolBudget) {
1845
+ step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
1846
+ statusPayload.toolBudget = step.toolBudget;
1847
+ }
1401
1848
  step.currentTool = event.toolName;
1402
1849
  step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
1403
1850
  step.currentToolStartedAt = now;
@@ -1419,6 +1866,15 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1419
1866
  const toolSnapshot = pendingToolResults[flatIndex];
1420
1867
  pendingToolResults[flatIndex] = undefined;
1421
1868
  const resultText = extractTextFromContent(event.message.content);
1869
+ if (toolSnapshot && resultText.includes("Tool budget hard limit reached")) {
1870
+ const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
1871
+ if (configuredToolBudget) {
1872
+ step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount ?? 0, toolSnapshot.tool);
1873
+ step.toolBudgetBlocked = true;
1874
+ statusPayload.toolBudget = step.toolBudget;
1875
+ statusPayload.toolBudgetBlocked = true;
1876
+ }
1877
+ }
1422
1878
  appendRecentStepOutput(step, resultText.split("\n").slice(-10));
1423
1879
  if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
1424
1880
  const state = mutatingFailureStates[flatIndex]!;
@@ -1469,6 +1925,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1469
1925
  statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
1470
1926
  }
1471
1927
  statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
1928
+ updateStepTurnBudget(flatIndex, step.turnCount, now, isTerminalAssistantStop(event.message));
1472
1929
  }
1473
1930
  syncTopLevelCurrentTool();
1474
1931
  step.lastActivityAt = now;
@@ -1566,17 +2023,68 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1566
2023
  ts: now,
1567
2024
  runId: id,
1568
2025
  }));
1569
- activeChildInterrupt?.();
2026
+ interruptNestedAsyncDescendants();
2027
+ interruptActiveChildren();
2028
+ };
2029
+ const timeoutRunner = () => {
2030
+ if (timedOut || interrupted || statusPayload.state !== "running") return;
2031
+ timedOut = true;
2032
+ const now = Date.now();
2033
+ const message = timeoutMessage ?? "Subagent timed out.";
2034
+ statusPayload.state = "failed";
2035
+ statusPayload.timedOut = true;
2036
+ statusPayload.error = message;
2037
+ currentActivityState = undefined;
2038
+ statusPayload.activityState = undefined;
2039
+ statusPayload.lastUpdate = now;
2040
+ for (const step of statusPayload.steps) {
2041
+ if (step.status !== "running" && step.status !== "pending") continue;
2042
+ step.status = "failed";
2043
+ step.error = message;
2044
+ step.exitCode = 1;
2045
+ step.timedOut = true;
2046
+ step.activityState = undefined;
2047
+ step.endedAt = now;
2048
+ step.durationMs = step.startedAt ? now - step.startedAt : 0;
2049
+ step.lastActivityAt = now;
2050
+ }
2051
+ writeStatusPayload();
2052
+ appendJsonl(eventsPath, JSON.stringify({
2053
+ type: "subagent.run.timed_out",
2054
+ ts: now,
2055
+ runId: id,
2056
+ timeoutMs: config.timeoutMs,
2057
+ deadlineAt: config.deadlineAt,
2058
+ message,
2059
+ }));
2060
+ timeoutAbortController.abort();
2061
+ timeoutNestedAsyncDescendants();
2062
+ timeoutActiveChildren();
1570
2063
  };
1571
2064
  process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
1572
- // Portable control inbox: the parent drops an interrupt request file here when
1573
- // it cannot deliver the OS signal (e.g. ENOSYS on Windows). Routes into the
1574
- // same graceful interruptRunner() so stop/steer work on every platform.
1575
- const disposeControlInbox = watchAsyncControlInbox(asyncDir, { onInterrupt: interruptRunner });
2065
+ // Portable control inbox: the parent drops control request files here when
2066
+ // it cannot deliver OS signals (e.g. ENOSYS on Windows) or when steering a
2067
+ // live child. Interrupts still route into the same graceful interruptRunner().
2068
+ const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
2069
+ onInterrupt: interruptRunner,
2070
+ onTimeout: timeoutRunner,
2071
+ onSteer: (request) => {
2072
+ const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
2073
+ if (targetStep?.status === "pending") pendingStepSteers.push(request);
2074
+ else if (request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) deliverSteerRequest(request);
2075
+ else pendingStepSteers.push(request);
2076
+ },
2077
+ });
2078
+ if (config.deadlineAt !== undefined) {
2079
+ const remainingMs = Math.max(0, config.deadlineAt - Date.now());
2080
+ timeoutTimer = setTimeout(timeoutRunner, remainingMs);
2081
+ timeoutTimer.unref?.();
2082
+ }
1576
2083
  appendJsonl(
1577
2084
  eventsPath,
1578
2085
  JSON.stringify({
1579
2086
  type: "subagent.run.started",
2087
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1580
2088
  ts: overallStartTime,
1581
2089
  runId: id,
1582
2090
  mode: statusPayload.mode,
@@ -1589,7 +2097,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1589
2097
  let stepCursor = 0;
1590
2098
 
1591
2099
  while (true) {
1592
- if (interrupted) break;
2100
+ if (interrupted || timedOut || turnBudgetExceeded) break;
1593
2101
  consumePendingAppendRequests();
1594
2102
  if (stepCursor >= steps.length) break;
1595
2103
  const stepIndex = stepCursor++;
@@ -1641,7 +2149,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1641
2149
  placeholder.durationMs = 0;
1642
2150
  }
1643
2151
  previousOutput = "Dynamic fanout produced 0 results.";
1644
- const groupAcceptance = step.effectiveAcceptance?.explicit
2152
+ const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
1645
2153
  ? await evaluateAcceptance({
1646
2154
  acceptance: step.effectiveAcceptance,
1647
2155
  output: "",
@@ -1650,39 +2158,59 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1650
2158
  notes: "Dynamic fanout produced 0 results.",
1651
2159
  }),
1652
2160
  cwd,
2161
+ signal: timeoutAbortController.signal,
2162
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1653
2163
  })
1654
2164
  : undefined;
1655
- if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1656
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1657
- if (groupAcceptanceFailure) {
2165
+ const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2166
+ const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2167
+ if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
2168
+ const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2169
+ if (groupTimedOut || groupAcceptanceFailure) {
2170
+ const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
1658
2171
  statusPayload.state = "failed";
1659
- statusPayload.error = groupAcceptanceFailure;
2172
+ statusPayload.error = errorMessage;
1660
2173
  if (placeholder) {
1661
2174
  placeholder.status = "failed";
1662
- placeholder.error = groupAcceptanceFailure;
2175
+ placeholder.error = errorMessage;
1663
2176
  placeholder.exitCode = 1;
2177
+ placeholder.timedOut = groupTimedOut ? true : undefined;
1664
2178
  }
1665
- markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1666
- statusPayload.lastUpdate = now;
2179
+ markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
2180
+ statusPayload.lastUpdate = Date.now();
1667
2181
  writeStatusPayload();
1668
- results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
2182
+ results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
1669
2183
  break;
1670
2184
  }
1671
2185
  flatIndex++;
1672
2186
  statusPayload.lastUpdate = now;
1673
- markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
2187
+ markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
1674
2188
  writeStatusPayload();
1675
2189
  continue;
1676
2190
  }
1677
2191
 
1678
- const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
1679
- ...step.parallel,
1680
- task: task.task ?? step.parallel.task,
1681
- label: task.label ?? step.parallel.label,
1682
- structuredOutput: undefined,
1683
- structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1684
- }));
1685
- const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
2192
+ const dynamicSteps = materialized.parallel.map((task, itemIndex) => {
2193
+ const thinkingOverride = step.thinkingOverrides?.[itemIndex];
2194
+ const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
2195
+ const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
2196
+ return {
2197
+ ...step.parallel,
2198
+ task: task.task ?? step.parallel.task,
2199
+ label: task.label ?? step.parallel.label,
2200
+ ...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
2201
+ ...(thinkingOverride ? {
2202
+ ...(model ? { model } : {}),
2203
+ ...(thinking ? { thinking } : {}),
2204
+ ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
2205
+ } : {}),
2206
+ structuredOutput: undefined,
2207
+ structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
2208
+ };
2209
+ });
2210
+ const dynamicFlatStepCount = Math.max(statusPayload.steps.length - 1 + dynamicSteps.length, 1);
2211
+ const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task, itemIndex) => {
2212
+ const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2213
+ return {
1686
2214
  agent: task.agent,
1687
2215
  phase: task.phase ?? step.phase,
1688
2216
  label: task.label,
@@ -1690,13 +2218,15 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1690
2218
  structured: Boolean(task.structuredOutputSchema),
1691
2219
  status: "pending",
1692
2220
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2221
+ ...(transcriptPath ? { transcriptPath } : {}),
1693
2222
  skills: task.skills,
1694
2223
  model: task.model,
1695
2224
  thinking: task.thinking,
1696
2225
  attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1697
2226
  recentTools: [],
1698
2227
  recentOutput: [],
1699
- }));
2228
+ };
2229
+ });
1700
2230
  statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
1701
2231
  if (config.childIntercomTargets) {
1702
2232
  config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
@@ -1745,6 +2275,8 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1745
2275
  let aborted = false;
1746
2276
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
1747
2277
  const fi = groupStartFlatIndex + taskIdx;
2278
+ if (timedOut) return timedOutStepResult(task.agent);
2279
+ if (interrupted) return pausedStepResult(task.agent);
1748
2280
  if (aborted && failFast) {
1749
2281
  const skippedAt = Date.now();
1750
2282
  statusPayload.steps[fi].status = "failed";
@@ -1770,6 +2302,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1770
2302
  statusPayload.lastUpdate = taskStartTime;
1771
2303
  writeStatusPayload();
1772
2304
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
2305
+ flushPendingStepSteers(fi);
1773
2306
  const singleResult = await runSingleStep(task, {
1774
2307
  previousOutput, placeholder, cwd, sessionEnabled,
1775
2308
  outputs,
@@ -1777,27 +2310,47 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1777
2310
  artifactsDir, artifactConfig, id,
1778
2311
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1779
2312
  outputFile: path.join(asyncDir, `output-${fi}.log`),
2313
+ steerInboxDir: stepSteerInboxDir(asyncDir, fi),
1780
2314
  piPackageRoot: config.piPackageRoot,
1781
2315
  piArgv1: config.piArgv1,
1782
2316
  childIntercomTarget: config.childIntercomTargets?.[fi],
1783
2317
  orchestratorIntercomTarget: config.controlIntercomTarget,
1784
2318
  nestedRoute: config.nestedRoute,
1785
- registerInterrupt: (interrupt) => {
1786
- activeChildInterrupt = interrupt;
1787
- },
2319
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2320
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2321
+ registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2322
+ timeoutSignal: timeoutAbortController.signal,
2323
+ timeoutMessage,
2324
+ turnBudget: config.turnBudget,
1788
2325
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1789
2326
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2327
+ skipAcceptance: () => timedOut,
1790
2328
  });
1791
2329
  const taskEndTime = Date.now();
1792
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
2330
+ const childInterrupted = singleResult.interrupted === true;
2331
+ statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
1793
2332
  statusPayload.steps[fi].endedAt = taskEndTime;
1794
2333
  statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1795
- statusPayload.steps[fi].exitCode = singleResult.exitCode;
2334
+ statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2335
+ statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2336
+ statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2337
+ statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2338
+ statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
2339
+ statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
2340
+ statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2341
+ if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2342
+ if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2343
+ if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2344
+ if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2345
+ if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
1796
2346
  statusPayload.steps[fi].model = singleResult.model;
1797
2347
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1798
2348
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1799
2349
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1800
- statusPayload.steps[fi].error = singleResult.error;
2350
+ statusPayload.steps[fi].totalCost = singleResult.totalCost;
2351
+ statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2352
+ statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2353
+ statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
1801
2354
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1802
2355
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1803
2356
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -1805,13 +2358,13 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1805
2358
  statusPayload.lastUpdate = taskEndTime;
1806
2359
  writeStatusPayload();
1807
2360
  appendJsonl(eventsPath, JSON.stringify({
1808
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2361
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1809
2362
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1810
- exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
2363
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1811
2364
  }));
1812
2365
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
1813
- return { ...singleResult, skipped: false };
1814
- });
2366
+ return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2367
+ }, globalSemaphore);
1815
2368
 
1816
2369
  flatIndex += dynamicSteps.length;
1817
2370
  for (const pr of parallelResults) {
@@ -1819,15 +2372,25 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1819
2372
  agent: pr.agent,
1820
2373
  output: pr.output,
1821
2374
  error: pr.error,
1822
- success: pr.exitCode === 0,
1823
- exitCode: pr.exitCode,
2375
+ success: pr.interrupted !== true && pr.exitCode === 0,
2376
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
1824
2377
  skipped: pr.skipped,
2378
+ interrupted: pr.interrupted,
2379
+ timedOut: pr.timedOut,
2380
+ turnBudget: pr.turnBudget,
2381
+ turnBudgetExceeded: pr.turnBudgetExceeded,
2382
+ wrapUpRequested: pr.wrapUpRequested,
2383
+ toolBudget: pr.toolBudget,
2384
+ toolBudgetBlocked: pr.toolBudgetBlocked,
1825
2385
  sessionFile: pr.sessionFile,
1826
2386
  intercomTarget: pr.intercomTarget,
1827
2387
  model: pr.model,
1828
2388
  attemptedModels: pr.attemptedModels,
1829
2389
  modelAttempts: pr.modelAttempts,
2390
+ totalCost: pr.totalCost,
1830
2391
  artifactPaths: pr.artifactPaths,
2392
+ transcriptPath: pr.transcriptPath,
2393
+ transcriptError: pr.transcriptError,
1831
2394
  structuredOutput: pr.structuredOutput,
1832
2395
  structuredOutputPath: pr.structuredOutputPath,
1833
2396
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -1846,7 +2409,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1846
2409
  stepIndex,
1847
2410
  };
1848
2411
  statusPayload.outputs = outputs;
1849
- const groupAcceptance = step.effectiveAcceptance
2412
+ const groupAcceptance = step.effectiveAcceptance && !timedOut
1850
2413
  ? await evaluateAcceptance({
1851
2414
  acceptance: step.effectiveAcceptance,
1852
2415
  output: "",
@@ -1855,21 +2418,27 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1855
2418
  notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1856
2419
  }),
1857
2420
  cwd,
2421
+ signal: timeoutAbortController.signal,
2422
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1858
2423
  })
1859
2424
  : undefined;
1860
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1861
- markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1862
- if (groupAcceptanceFailure) {
2425
+ const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2426
+ const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2427
+ const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2428
+ const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2429
+ markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
2430
+ if (groupError) {
1863
2431
  results.push({
1864
2432
  agent: step.parallel.agent,
1865
- output: groupAcceptanceFailure,
1866
- error: groupAcceptanceFailure,
2433
+ output: groupError,
2434
+ error: groupError,
1867
2435
  success: false,
1868
2436
  exitCode: 1,
2437
+ timedOut: groupTimedOut ? true : undefined,
1869
2438
  structuredOutput: collection,
1870
- acceptance: groupAcceptance,
2439
+ acceptance: effectiveGroupAcceptance,
1871
2440
  });
1872
- statusPayload.error = groupAcceptanceFailure;
2441
+ statusPayload.error = groupError;
1873
2442
  }
1874
2443
  } catch (error) {
1875
2444
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -1935,6 +2504,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1935
2504
  setupHook: config.worktreeSetupHook
1936
2505
  ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
1937
2506
  : undefined,
2507
+ baseDir: config.worktreeBaseDir,
1938
2508
  });
1939
2509
  } catch (error) {
1940
2510
  const setupError = error instanceof Error ? error.message : String(error);
@@ -1976,6 +2546,8 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
1976
2546
  concurrency,
1977
2547
  async (task, taskIdx) => {
1978
2548
  const fi = groupStartFlatIndex + taskIdx;
2549
+ if (timedOut) return timedOutStepResult(task.agent);
2550
+ if (interrupted) return pausedStepResult(task.agent);
1979
2551
  if (aborted && failFast) {
1980
2552
  const skippedAt = Date.now();
1981
2553
  statusPayload.steps[fi].status = "failed";
@@ -2016,6 +2588,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2016
2588
  ? path.join(config.sessionDir, `parallel-${taskIdx}`)
2017
2589
  : undefined;
2018
2590
  const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
2591
+ flushPendingStepSteers(fi);
2019
2592
 
2020
2593
  const singleResult = await runSingleStep(taskForRun, {
2021
2594
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
@@ -2024,16 +2597,21 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2024
2597
  artifactsDir, artifactConfig, id,
2025
2598
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
2026
2599
  outputFile: path.join(asyncDir, `output-${fi}.log`),
2600
+ steerInboxDir: stepSteerInboxDir(asyncDir, fi),
2027
2601
  piPackageRoot: config.piPackageRoot,
2028
2602
  piArgv1: config.piArgv1,
2029
2603
  childIntercomTarget: config.childIntercomTargets?.[fi],
2030
2604
  orchestratorIntercomTarget: config.controlIntercomTarget,
2031
2605
  nestedRoute: config.nestedRoute,
2032
- registerInterrupt: (interrupt) => {
2033
- activeChildInterrupt = interrupt;
2034
- },
2606
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2607
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2608
+ registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2609
+ timeoutSignal: timeoutAbortController.signal,
2610
+ timeoutMessage,
2611
+ turnBudget: config.turnBudget,
2035
2612
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2036
2613
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2614
+ skipAcceptance: () => timedOut,
2037
2615
  });
2038
2616
  if (task.sessionFile) {
2039
2617
  latestSessionFile = task.sessionFile;
@@ -2041,16 +2619,31 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2041
2619
 
2042
2620
  const taskEndTime = Date.now();
2043
2621
  const taskDuration = taskEndTime - taskStartTime;
2622
+ const childInterrupted = singleResult.interrupted === true;
2044
2623
 
2045
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
2624
+ statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2046
2625
  statusPayload.steps[fi].endedAt = taskEndTime;
2047
2626
  statusPayload.steps[fi].durationMs = taskDuration;
2048
- statusPayload.steps[fi].exitCode = singleResult.exitCode;
2627
+ statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2628
+ statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2629
+ statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2630
+ statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2631
+ statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
2632
+ statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
2633
+ statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2634
+ if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2635
+ if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2636
+ if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2637
+ if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2638
+ if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
2049
2639
  statusPayload.steps[fi].model = singleResult.model;
2050
2640
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2051
2641
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2052
2642
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2053
- statusPayload.steps[fi].error = singleResult.error;
2643
+ statusPayload.steps[fi].totalCost = singleResult.totalCost;
2644
+ statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2645
+ statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2646
+ statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
2054
2647
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2055
2648
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2056
2649
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2059,9 +2652,9 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2059
2652
  writeStatusPayload();
2060
2653
 
2061
2654
  appendJsonl(eventsPath, JSON.stringify({
2062
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2655
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2063
2656
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2064
- exitCode: singleResult.exitCode, durationMs: taskDuration,
2657
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
2065
2658
  }));
2066
2659
  if (singleResult.completionGuardTriggered) {
2067
2660
  const event = buildControlEvent({
@@ -2078,8 +2671,9 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2078
2671
  }
2079
2672
 
2080
2673
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
2081
- return { ...singleResult, skipped: false };
2674
+ return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2082
2675
  },
2676
+ globalSemaphore,
2083
2677
  );
2084
2678
 
2085
2679
  flatIndex += group.parallel.length;
@@ -2107,15 +2701,25 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2107
2701
  agent: pr.agent,
2108
2702
  output: pr.output,
2109
2703
  error: pr.error,
2110
- success: pr.exitCode === 0,
2111
- exitCode: pr.exitCode,
2704
+ success: pr.interrupted !== true && pr.exitCode === 0,
2705
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2112
2706
  skipped: pr.skipped,
2707
+ interrupted: pr.interrupted,
2708
+ timedOut: pr.timedOut,
2709
+ turnBudget: pr.turnBudget,
2710
+ turnBudgetExceeded: pr.turnBudgetExceeded,
2711
+ wrapUpRequested: pr.wrapUpRequested,
2712
+ toolBudget: pr.toolBudget,
2713
+ toolBudgetBlocked: pr.toolBudgetBlocked,
2113
2714
  sessionFile: pr.sessionFile,
2114
2715
  intercomTarget: pr.intercomTarget,
2115
2716
  model: pr.model,
2116
2717
  attemptedModels: pr.attemptedModels,
2117
2718
  modelAttempts: pr.modelAttempts,
2719
+ totalCost: pr.totalCost,
2118
2720
  artifactPaths: pr.artifactPaths,
2721
+ transcriptPath: pr.transcriptPath,
2722
+ transcriptError: pr.transcriptError,
2119
2723
  structuredOutput: pr.structuredOutput,
2120
2724
  structuredOutputPath: pr.structuredOutputPath,
2121
2725
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
@@ -2182,6 +2786,7 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2182
2786
  agent: seqStep.agent,
2183
2787
  }));
2184
2788
 
2789
+ flushPendingStepSteers(flatIndex);
2185
2790
  const singleResult = await runSingleStep(seqStep, {
2186
2791
  previousOutput, placeholder, cwd, sessionEnabled,
2187
2792
  outputs,
@@ -2189,16 +2794,21 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2189
2794
  artifactsDir, artifactConfig, id,
2190
2795
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2191
2796
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2797
+ steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
2192
2798
  piPackageRoot: config.piPackageRoot,
2193
2799
  piArgv1: config.piArgv1,
2194
2800
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2195
2801
  orchestratorIntercomTarget: config.controlIntercomTarget,
2196
2802
  nestedRoute: config.nestedRoute,
2197
- registerInterrupt: (interrupt) => {
2198
- activeChildInterrupt = interrupt;
2199
- },
2803
+ registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
2804
+ registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
2805
+ registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
2806
+ timeoutSignal: timeoutAbortController.signal,
2807
+ timeoutMessage,
2808
+ turnBudget: config.turnBudget,
2200
2809
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2201
2810
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2811
+ skipAcceptance: () => timedOut,
2202
2812
  });
2203
2813
  if (seqStep.sessionFile) {
2204
2814
  latestSessionFile = seqStep.sessionFile;
@@ -2207,20 +2817,30 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2207
2817
  previousOutput = singleResult.output;
2208
2818
  results.push({
2209
2819
  agent: singleResult.agent,
2210
- output: singleResult.output,
2211
- error: singleResult.error,
2212
- success: singleResult.exitCode === 0,
2213
- exitCode: singleResult.exitCode,
2820
+ output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
2821
+ error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
2822
+ success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
2823
+ exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
2214
2824
  sessionFile: singleResult.sessionFile,
2215
2825
  intercomTarget: singleResult.intercomTarget,
2216
2826
  model: singleResult.model,
2217
2827
  attemptedModels: singleResult.attemptedModels,
2218
2828
  modelAttempts: singleResult.modelAttempts,
2829
+ totalCost: singleResult.totalCost,
2219
2830
  artifactPaths: singleResult.artifactPaths,
2831
+ transcriptPath: singleResult.transcriptPath,
2832
+ transcriptError: singleResult.transcriptError,
2220
2833
  structuredOutput: singleResult.structuredOutput,
2221
2834
  structuredOutputPath: singleResult.structuredOutputPath,
2222
2835
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2223
2836
  acceptance: singleResult.acceptance,
2837
+ interrupted: singleResult.interrupted,
2838
+ timedOut: timedOut || singleResult.timedOut ? true : undefined,
2839
+ turnBudget: singleResult.turnBudget,
2840
+ turnBudgetExceeded: singleResult.turnBudgetExceeded,
2841
+ wrapUpRequested: singleResult.wrapUpRequested,
2842
+ toolBudget: singleResult.toolBudget,
2843
+ toolBudgetBlocked: singleResult.toolBudgetBlocked,
2224
2844
  });
2225
2845
  if (seqStep.outputName) {
2226
2846
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
@@ -2253,15 +2873,30 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2253
2873
  }
2254
2874
 
2255
2875
  const stepEndTime = Date.now();
2256
- statusPayload.steps[flatIndex].status = singleResult.exitCode === 0 ? "complete" : "failed";
2876
+ const childInterrupted = singleResult.interrupted === true;
2877
+ statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2257
2878
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
2258
2879
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2259
- statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
2880
+ statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2881
+ statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2882
+ statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
2883
+ statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2884
+ statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
2885
+ statusPayload.steps[flatIndex].toolBudget = singleResult.toolBudget;
2886
+ statusPayload.steps[flatIndex].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2887
+ if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2888
+ if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2889
+ if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2890
+ if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2891
+ if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
2260
2892
  statusPayload.steps[flatIndex].model = singleResult.model;
2261
2893
  statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
2262
2894
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
2263
2895
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
2264
- statusPayload.steps[flatIndex].error = singleResult.error;
2896
+ statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
2897
+ statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2898
+ statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
2899
+ statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
2265
2900
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2266
2901
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2267
2902
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2274,12 +2909,12 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2274
2909
  writeStatusPayload();
2275
2910
 
2276
2911
  appendJsonl(eventsPath, JSON.stringify({
2277
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2912
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2278
2913
  ts: stepEndTime,
2279
2914
  runId: id,
2280
2915
  stepIndex: flatIndex,
2281
2916
  agent: seqStep.agent,
2282
- exitCode: singleResult.exitCode,
2917
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
2283
2918
  durationMs: stepEndTime - stepStartTime,
2284
2919
  tokens: stepTokens,
2285
2920
  }));
@@ -2318,6 +2953,12 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2318
2953
  }
2319
2954
 
2320
2955
  const resultMode = config.resultMode ?? statusPayload.mode;
2956
+ const totalCost = results.reduce<CostSummary>((sum, result) => ({
2957
+ inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
2958
+ outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
2959
+ costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
2960
+ }), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
2961
+ const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
2321
2962
  const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2322
2963
  const agentName = finalFlatAgents.length === 1
2323
2964
  ? finalFlatAgents[0]!
@@ -2358,14 +2999,27 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2358
2999
  clearInterval(activityTimer);
2359
3000
  activityTimer = undefined;
2360
3001
  }
3002
+ if (timeoutTimer) {
3003
+ clearTimeout(timeoutTimer);
3004
+ timeoutTimer = undefined;
3005
+ }
2361
3006
  disposeControlInbox();
2362
3007
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
2363
3008
  const runEndedAt = Date.now();
2364
- statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3009
+ statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
2365
3010
  statusPayload.activityState = undefined;
3011
+ if (timedOut) {
3012
+ statusPayload.timedOut = true;
3013
+ statusPayload.error = timeoutMessage ?? "Subagent timed out.";
3014
+ }
3015
+ if (turnBudgetExceeded && !statusPayload.error) {
3016
+ const budget = statusPayload.turnBudget;
3017
+ statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
3018
+ }
2366
3019
  statusPayload.endedAt = runEndedAt;
2367
3020
  statusPayload.lastUpdate = runEndedAt;
2368
3021
  statusPayload.sessionFile = effectiveSessionFile;
3022
+ statusPayload.totalCost = finalTotalCost;
2369
3023
  statusPayload.shareUrl = shareUrl;
2370
3024
  statusPayload.gistUrl = gistUrl;
2371
3025
  statusPayload.shareError = shareError;
@@ -2380,10 +3034,13 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2380
3034
  eventsPath,
2381
3035
  JSON.stringify({
2382
3036
  type: "subagent.run.completed",
3037
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2383
3038
  ts: runEndedAt,
2384
3039
  runId: id,
2385
3040
  status: statusPayload.state,
2386
3041
  durationMs: runEndedAt - overallStartTime,
3042
+ totalTokens: statusPayload.totalTokens,
3043
+ totalCost: finalTotalCost,
2387
3044
  }),
2388
3045
  );
2389
3046
  writeRunLog(logPath, {
@@ -2407,25 +3064,44 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2407
3064
 
2408
3065
  try {
2409
3066
  writeAtomicJson(resultPath, {
3067
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2410
3068
  id,
2411
3069
  agent: agentName,
2412
3070
  mode: resultMode,
2413
- success: !interrupted && results.every((r) => r.success),
2414
- state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
2415
- summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
3071
+ success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
3072
+ state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
3073
+ summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
3074
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
3075
+ ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
3076
+ ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
3077
+ ...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
3078
+ ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
3079
+ ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
3080
+ ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
3081
+ ...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
2416
3082
  results: results.map((r) => ({
2417
3083
  agent: r.agent,
2418
3084
  output: r.output,
2419
3085
  error: r.error,
2420
3086
  success: r.success,
2421
3087
  skipped: r.skipped || undefined,
3088
+ interrupted: r.interrupted || undefined,
3089
+ timedOut: r.timedOut || undefined,
3090
+ turnBudget: r.turnBudget,
3091
+ turnBudgetExceeded: r.turnBudgetExceeded || undefined,
3092
+ wrapUpRequested: r.wrapUpRequested || undefined,
3093
+ toolBudget: r.toolBudget,
3094
+ toolBudgetBlocked: r.toolBudgetBlocked || undefined,
2422
3095
  sessionFile: r.sessionFile,
2423
3096
  intercomTarget: r.intercomTarget,
2424
3097
  model: r.model,
2425
3098
  attemptedModels: r.attemptedModels,
2426
3099
  modelAttempts: r.modelAttempts,
3100
+ totalCost: r.totalCost,
2427
3101
  artifactPaths: r.artifactPaths,
2428
3102
  truncated: r.truncated,
3103
+ transcriptPath: r.transcriptPath,
3104
+ transcriptError: r.transcriptError,
2429
3105
  structuredOutput: r.structuredOutput,
2430
3106
  structuredOutputPath: r.structuredOutputPath,
2431
3107
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
@@ -2433,9 +3109,11 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2433
3109
  })),
2434
3110
  outputs,
2435
3111
  workflowGraph: statusPayload.workflowGraph,
2436
- exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
3112
+ exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
2437
3113
  timestamp: runEndedAt,
2438
3114
  durationMs: runEndedAt - overallStartTime,
3115
+ totalTokens: statusPayload.totalTokens,
3116
+ totalCost: finalTotalCost,
2439
3117
  truncated,
2440
3118
  artifactsDir,
2441
3119
  cwd,
@@ -2454,49 +3132,40 @@ export async function runSubagent(config: SubagentRunConfig): Promise<void> {
2454
3132
  }
2455
3133
  }
2456
3134
 
2457
- export async function runSubagentFromConfigFile(cfgPath: string): Promise<void> {
2458
- const configJson = fs.readFileSync(cfgPath, "utf-8");
2459
- const config = JSON.parse(configJson) as SubagentRunConfig;
3135
+ const configArg = process.argv[2];
3136
+ if (configArg) {
2460
3137
  try {
2461
- fs.unlinkSync(cfgPath);
2462
- } catch {
2463
- // Temp config cleanup is best effort.
2464
- }
2465
- await runSubagent(config);
2466
- }
2467
-
2468
- function isRunnerMainModule(): boolean {
2469
- try {
2470
- return import.meta.url === pathToFileURL(process.argv[1] ?? "").href;
2471
- } catch {
2472
- return false;
2473
- }
2474
- }
2475
-
2476
- if (!runnerConfigPath && isRunnerMainModule()) {
2477
- const configArg = process.argv[2];
2478
- if (configArg) {
2479
- runSubagentFromConfigFile(configArg).catch((err) => {
2480
- console.error("Subagent runner error:", err);
3138
+ const configJson = fs.readFileSync(configArg, "utf-8");
3139
+ const config = JSON.parse(configJson) as SubagentRunConfig;
3140
+ try {
3141
+ fs.unlinkSync(configArg);
3142
+ } catch {
3143
+ // Temp config cleanup is best effort.
3144
+ }
3145
+ runSubagent(config).catch((runErr) => {
3146
+ console.error("Subagent runner error:", runErr);
2481
3147
  process.exit(1);
2482
3148
  });
2483
- } else {
2484
- let input = "";
2485
- process.stdin.setEncoding("utf-8");
2486
- process.stdin.on("data", (chunk) => {
2487
- input += chunk;
2488
- });
2489
- process.stdin.on("end", () => {
2490
- try {
2491
- const config = JSON.parse(input) as SubagentRunConfig;
2492
- runSubagent(config).catch((err) => {
2493
- console.error("Subagent runner error:", err);
2494
- process.exit(1);
2495
- });
2496
- } catch (err) {
2497
- console.error("Subagent runner error:", err);
2498
- process.exit(1);
2499
- }
2500
- });
3149
+ } catch (err) {
3150
+ console.error("Subagent runner error:", err);
3151
+ process.exit(1);
2501
3152
  }
3153
+ } else {
3154
+ let input = "";
3155
+ process.stdin.setEncoding("utf-8");
3156
+ process.stdin.on("data", (chunk) => {
3157
+ input += chunk;
3158
+ });
3159
+ process.stdin.on("end", () => {
3160
+ try {
3161
+ const config = JSON.parse(input) as SubagentRunConfig;
3162
+ runSubagent(config).catch((runErr) => {
3163
+ console.error("Subagent runner error:", runErr);
3164
+ process.exit(1);
3165
+ });
3166
+ } catch (err) {
3167
+ console.error("Subagent runner error:", err);
3168
+ process.exit(1);
3169
+ }
3170
+ });
2502
3171
  }