@selesai/code 0.5.27 → 0.5.29

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 (310) hide show
  1. package/dist/cli/args.d.ts.map +1 -1
  2. package/dist/cli/args.js +9 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +1 -1
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/credential-print.d.ts +23 -0
  8. package/dist/cli/credential-print.d.ts.map +1 -0
  9. package/dist/cli/credential-print.js +117 -0
  10. package/dist/cli/credential-print.js.map +1 -0
  11. package/dist/cli/startup-ui.d.ts.map +1 -1
  12. package/dist/cli/startup-ui.js +1 -1
  13. package/dist/cli/startup-ui.js.map +1 -1
  14. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  15. package/dist/core/agent-session-runtime.js +3 -0
  16. package/dist/core/agent-session-runtime.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +12 -1
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +20 -14
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction/compaction.d.ts.map +1 -1
  22. package/dist/core/compaction/compaction.js +11 -3
  23. package/dist/core/compaction/compaction.js.map +1 -1
  24. package/dist/core/extensions/runner.d.ts +1 -0
  25. package/dist/core/extensions/runner.d.ts.map +1 -1
  26. package/dist/core/extensions/runner.js +11 -0
  27. package/dist/core/extensions/runner.js.map +1 -1
  28. package/dist/core/extensions/types.d.ts +14 -1
  29. package/dist/core/extensions/types.d.ts.map +1 -1
  30. package/dist/core/extensions/types.js.map +1 -1
  31. package/dist/core/footer-data-provider.d.ts +10 -0
  32. package/dist/core/footer-data-provider.d.ts.map +1 -1
  33. package/dist/core/footer-data-provider.js +1 -1
  34. package/dist/core/footer-data-provider.js.map +1 -1
  35. package/dist/core/llama/provider.d.ts.map +1 -1
  36. package/dist/core/llama/provider.js +8 -3
  37. package/dist/core/llama/provider.js.map +1 -1
  38. package/dist/core/model-config.d.ts +30 -0
  39. package/dist/core/model-config.d.ts.map +1 -1
  40. package/dist/core/model-config.js +6 -0
  41. package/dist/core/model-config.js.map +1 -1
  42. package/dist/core/model-registry.d.ts.map +1 -1
  43. package/dist/core/model-registry.js +2 -2
  44. package/dist/core/model-registry.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +1 -0
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +20 -3
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/model-runtime.d.ts +2 -0
  50. package/dist/core/model-runtime.d.ts.map +1 -1
  51. package/dist/core/model-runtime.js +5 -4
  52. package/dist/core/model-runtime.js.map +1 -1
  53. package/dist/core/package-manager.d.ts.map +1 -1
  54. package/dist/core/package-manager.js +13 -6
  55. package/dist/core/package-manager.js.map +1 -1
  56. package/dist/core/remote-catalog-provider.d.ts +1 -1
  57. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  58. package/dist/core/remote-catalog-provider.js +24 -11
  59. package/dist/core/remote-catalog-provider.js.map +1 -1
  60. package/dist/core/resource-loader.d.ts +15 -0
  61. package/dist/core/resource-loader.d.ts.map +1 -1
  62. package/dist/core/resource-loader.js +66 -9
  63. package/dist/core/resource-loader.js.map +1 -1
  64. package/dist/core/settings-manager.d.ts +1 -1
  65. package/dist/core/settings-manager.d.ts.map +1 -1
  66. package/dist/core/settings-manager.js.map +1 -1
  67. package/dist/core/system-prompt.d.ts.map +1 -1
  68. package/dist/core/system-prompt.js +1 -1
  69. package/dist/core/system-prompt.js.map +1 -1
  70. package/dist/core/tools/bash.d.ts +2 -0
  71. package/dist/core/tools/bash.d.ts.map +1 -1
  72. package/dist/core/tools/bash.js +34 -5
  73. package/dist/core/tools/bash.js.map +1 -1
  74. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  75. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  76. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  77. package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
  78. package/dist/extensions/pi-intercom/README.md +102 -39
  79. package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
  80. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
  81. package/dist/extensions/pi-intercom/broker/client.ts +315 -12
  82. package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
  83. package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
  84. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
  85. package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
  86. package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
  87. package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
  88. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
  89. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
  90. package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
  91. package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
  92. package/dist/extensions/pi-intercom/config.test.ts +93 -0
  93. package/dist/extensions/pi-intercom/config.ts +55 -6
  94. package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
  95. package/dist/extensions/pi-intercom/cwd.ts +31 -0
  96. package/dist/extensions/pi-intercom/extension-api.ts +44 -0
  97. package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
  98. package/dist/extensions/pi-intercom/format-context.ts +32 -0
  99. package/dist/extensions/pi-intercom/index.ts +742 -145
  100. package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
  101. package/dist/extensions/pi-intercom/package.json +15 -5
  102. package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
  103. package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
  104. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
  105. package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
  106. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
  107. package/dist/extensions/pi-intercom/types.ts +94 -4
  108. package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
  109. package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
  110. package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
  111. package/dist/extensions/pi-subagents/CHANGELOG.md +85 -0
  112. package/dist/extensions/pi-subagents/LICENSE +21 -0
  113. package/dist/extensions/pi-subagents/README.md +146 -52
  114. package/dist/extensions/pi-subagents/agents/architect.md +1 -0
  115. package/dist/extensions/pi-subagents/agents/builder.md +1 -0
  116. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  117. package/dist/extensions/pi-subagents/package.json +3 -3
  118. package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
  119. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +21 -989
  120. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  121. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +430 -0
  122. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  123. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +282 -0
  124. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +71 -30
  125. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
  126. package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
  127. package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
  128. package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
  129. package/dist/extensions/pi-subagents/src/api/preflight.ts +16 -12
  130. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
  131. package/dist/extensions/pi-subagents/src/extension/index.ts +17 -7
  132. package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
  133. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -6
  134. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +8 -7
  135. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
  136. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
  137. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +63 -16
  138. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
  139. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
  140. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  141. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
  142. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
  143. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +29 -4
  144. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +29 -6
  145. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
  146. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +372 -28
  147. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
  148. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
  149. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +143 -43
  150. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +527 -247
  151. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
  152. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +426 -123
  153. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +15 -7
  154. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
  155. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
  156. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  157. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
  158. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
  159. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
  160. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
  161. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
  162. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
  163. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
  164. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  165. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
  166. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
  167. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +1 -1
  168. package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
  169. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
  170. package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
  171. package/dist/extensions/pi-subagents/src/shared/types.ts +155 -10
  172. package/dist/extensions/pi-subagents/src/shared/utils.ts +73 -4
  173. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -0
  174. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
  175. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
  176. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
  177. package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
  178. package/dist/extensions/pi-subagents/src/tui/render.ts +27 -28
  179. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
  180. package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
  181. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
  182. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
  183. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
  184. package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
  185. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  186. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +348 -7
  187. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
  188. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
  189. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
  190. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +26 -1
  191. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
  192. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +213 -13
  194. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +34 -0
  195. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
  196. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +118 -12
  197. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +850 -7
  198. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
  199. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
  200. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  201. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
  202. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
  203. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +128 -0
  204. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
  205. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
  206. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
  207. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +102 -0
  208. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
  209. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
  210. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
  211. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
  212. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
  213. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
  214. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +4 -2
  215. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
  216. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
  217. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
  218. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
  219. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +11 -11
  220. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
  221. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
  222. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
  223. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +10 -2
  224. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
  225. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
  226. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
  227. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +46 -0
  228. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
  229. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
  230. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
  231. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
  232. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
  233. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +34 -2
  234. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  235. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
  236. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
  237. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
  238. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
  239. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +3 -0
  240. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +1 -1
  241. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
  245. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
  246. package/dist/extensions/question/index.ts +14 -1
  247. package/dist/extensions/question/tests/wizard.test.ts +11 -8
  248. package/dist/main.d.ts.map +1 -1
  249. package/dist/main.js +43 -0
  250. package/dist/main.js.map +1 -1
  251. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  252. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  253. package/dist/modes/interactive/components/custom-message.js +10 -2
  254. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  255. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  256. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  257. package/dist/modes/interactive/components/extension-editor.js +16 -46
  258. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  259. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  260. package/dist/modes/interactive/components/model-selector.js +4 -1
  261. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  262. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  263. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  264. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  265. package/dist/modes/interactive/external-editor.d.ts +12 -0
  266. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  267. package/dist/modes/interactive/external-editor.js +37 -0
  268. package/dist/modes/interactive/external-editor.js.map +1 -0
  269. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  270. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  271. package/dist/modes/interactive/interactive-mode.js +72 -64
  272. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  273. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  274. package/dist/modes/rpc/rpc-mode.js +14 -0
  275. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  276. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  277. package/dist/utils/clipboard.d.ts.map +1 -1
  278. package/dist/utils/clipboard.js +19 -8
  279. package/dist/utils/clipboard.js.map +1 -1
  280. package/dist/utils/version-check.d.ts.map +1 -1
  281. package/dist/utils/version-check.js +1 -1
  282. package/dist/utils/version-check.js.map +1 -1
  283. package/docs/compaction.md +1 -1
  284. package/docs/custom-provider.md +14 -5
  285. package/docs/environment-variables.md +86 -0
  286. package/docs/extensions.md +15 -5
  287. package/docs/index.md +1 -0
  288. package/docs/models.md +9 -2
  289. package/docs/providers.md +21 -2
  290. package/docs/rpc.md +23 -6
  291. package/docs/session-format.md +2 -0
  292. package/docs/settings.md +1 -1
  293. package/docs/usage.md +0 -13
  294. package/examples/extensions/custom-compaction.ts +5 -2
  295. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  296. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  297. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  298. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  299. package/examples/extensions/gondolin/package-lock.json +2 -2
  300. package/examples/extensions/gondolin/package.json +1 -1
  301. package/examples/extensions/handoff.ts +11 -3
  302. package/examples/extensions/message-renderer.ts +3 -3
  303. package/examples/extensions/sandbox/package-lock.json +2 -2
  304. package/examples/extensions/sandbox/package.json +1 -1
  305. package/examples/extensions/summarize.ts +5 -2
  306. package/examples/extensions/with-deps/package-lock.json +2 -2
  307. package/examples/extensions/with-deps/package.json +1 -1
  308. package/examples/sdk/12-full-control.ts +3 -1
  309. package/package.json +15 -6
  310. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -45,6 +45,9 @@ import {
45
45
  hasEmptyTerminalAssistantResponse,
46
46
  extractToolArgsPreview,
47
47
  extractTextFromContent,
48
+ boundStreamedRecentTools,
49
+ boundStreamedRecentOutput,
50
+ boundStreamedToolCalls,
48
51
  } from "../../shared/utils.ts";
49
52
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
50
53
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
@@ -52,10 +55,12 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
52
55
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
53
56
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
54
57
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
55
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
56
- import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
58
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
59
+ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
60
+ import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
57
61
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
58
62
  import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
63
+ import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
59
64
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
60
65
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
61
66
  import {
@@ -63,6 +68,13 @@ import {
63
68
  formatModelAttemptNote,
64
69
  isRetryableModelFailure,
65
70
  } from "../shared/model-fallback.ts";
71
+ import {
72
+ SUBAGENT_STARTUP_RETRY_DELAYS_MS,
73
+ formatSubagentStartupRetryExhaustedError,
74
+ formatSubagentStartupRetryNote,
75
+ isRetryableSubagentStartupFailure,
76
+ waitForSubagentStartupRetry,
77
+ } from "../shared/subagent-startup-retry.ts";
66
78
  import {
67
79
  createMutatingFailureState,
68
80
  didMutatingToolFail,
@@ -111,6 +123,47 @@ function sumUsage(target: Usage, source: Usage): void {
111
123
  target.turns += source.turns;
112
124
  }
113
125
 
126
+ function persistSingleResultMetadata(input: {
127
+ metadataPath?: string;
128
+ enabled: boolean;
129
+ runId?: string;
130
+ agent: string;
131
+ task: string;
132
+ result: SingleResult;
133
+ }): void {
134
+ if (!input.enabled || !input.metadataPath) return;
135
+ const target = input.result;
136
+ writeMetadata(input.metadataPath, {
137
+ runId: input.runId,
138
+ agent: input.agent,
139
+ task: input.task,
140
+ exitCode: target.exitCode,
141
+ processSignal: target.processSignal,
142
+ usage: target.usage,
143
+ model: target.model,
144
+ attemptedModels: target.attemptedModels,
145
+ modelAttempts: target.modelAttempts,
146
+ durationMs: target.progressSummary?.durationMs,
147
+ toolCount: target.progressSummary?.toolCount,
148
+ error: target.error,
149
+ agentContract: target.agentContract,
150
+ launchContractDigest: target.launchContractDigest,
151
+ launchResolvedExtensions: target.launchResolvedExtensions,
152
+ runtimeAcknowledgedExtensions: target.runtimeAcknowledgedExtensions,
153
+ execution: target.execution,
154
+ acceptance: target.acceptance,
155
+ capabilityCeiling: target.capabilityCeiling,
156
+ capabilityAudit: target.capabilityAudit,
157
+ review: target.review,
158
+ effects: target.effects,
159
+ transcriptPath: target.transcriptPath,
160
+ transcriptError: target.transcriptError,
161
+ skills: target.skills,
162
+ skillsWarning: target.skillsWarning,
163
+ timestamp: Date.now(),
164
+ });
165
+ }
166
+
114
167
  function formatTimeoutMessage(timeoutMs: number): string {
115
168
  return `Subagent timed out after ${timeoutMs}ms.`;
116
169
  }
@@ -138,6 +191,21 @@ function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): Acc
138
191
  };
139
192
  }
140
193
 
194
+ function buildInterruptedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
195
+ const pending = buildPendingAcceptanceLedger(acceptance);
196
+ if (acceptance.level === "none") {
197
+ pending.status = "not-required";
198
+ pending.evidenceStatus = "not-required";
199
+ } else {
200
+ pending.runtimeChecks = [{
201
+ id: "interrupted",
202
+ status: "not-applicable",
203
+ message: "Acceptance was not evaluated because the subagent was interrupted.",
204
+ }];
205
+ }
206
+ return pending;
207
+ }
208
+
141
209
  function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
142
210
  if (lines.length === 0) return;
143
211
  progress.recentOutput.push(...lines.filter((line) => line.trim()));
@@ -161,8 +229,8 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
161
229
  return {
162
230
  ...progress,
163
231
  skills: progress.skills ? [...progress.skills] : undefined,
164
- recentTools: progress.recentTools.map((tool) => ({ ...tool })),
165
- recentOutput: [...progress.recentOutput],
232
+ recentTools: boundStreamedRecentTools(progress.recentTools),
233
+ recentOutput: boundStreamedRecentOutput(progress.recentOutput),
166
234
  };
167
235
  }
168
236
 
@@ -188,6 +256,20 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
188
256
  };
189
257
  }
190
258
 
259
+ /**
260
+ * Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
261
+ * unbounded `messages` transcript in favour of compact tool-call summaries so a
262
+ * single `tool_execution_update` line stays well under the child-stdout protocol
263
+ * cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
264
+ * foreground detach receipts and terminal consumers use snapshotResult directly.
265
+ */
266
+ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
267
+ const snapshot = snapshotResult(result, progress);
268
+ snapshot.messages = undefined;
269
+ snapshot.toolCalls = boundStreamedToolCalls(result);
270
+ return snapshot;
271
+ }
272
+
191
273
  async function runSingleAttempt(
192
274
  runtimeCwd: string,
193
275
  agent: AgentConfig,
@@ -220,7 +302,7 @@ async function runSingleAttempt(
220
302
  childIndex: options.index ?? 0,
221
303
  })
222
304
  : undefined;
223
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
305
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
224
306
  baseArgs: ["--offline", "--mode", "json", "-p"],
225
307
  task,
226
308
  sessionEnabled: shared.sessionEnabled,
@@ -268,7 +350,9 @@ async function runSingleAttempt(
268
350
  structuredOutput: Boolean(options.structuredOutput),
269
351
  capabilityCeiling: options.capabilityCeiling,
270
352
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
353
+ agentName: agent.name,
271
354
  });
355
+ const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
272
356
  const launchContractDigest = launchBindingDigest({
273
357
  definitionDigest: agentDefinitionDigest(agent),
274
358
  task: shared.originalTask ?? task,
@@ -288,11 +372,14 @@ async function runSingleAttempt(
288
372
  ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
289
373
  });
290
374
  const result: SingleResult = withRunContext({
375
+ index: options.index ?? 0,
291
376
  agent: agent.name,
292
377
  task: shared.originalTask ?? task,
293
378
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
294
379
  launchContractDigest,
380
+ launchResolvedExtensions,
295
381
  exitCode: 0,
382
+ outputState: "absent",
296
383
  messages: [],
297
384
  usage: emptyUsage(),
298
385
  model: modelArg,
@@ -337,6 +424,10 @@ async function runSingleAttempt(
337
424
  recentOutput: [...shared.attemptNotes],
338
425
  toolCount: 0,
339
426
  tokens: 0,
427
+ ...(modelArg ? { model: modelArg } : {}),
428
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
429
+ inputTokens: 0,
430
+ outputTokens: 0,
340
431
  durationMs: 0,
341
432
  lastActivityAt: startTime,
342
433
  };
@@ -371,7 +462,7 @@ async function runSingleAttempt(
371
462
  });
372
463
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
373
464
  let processClosed = false;
374
- let settled = false;
465
+ let lifecycleFinished = false;
375
466
  let detached = false;
376
467
  let intercomStarted = false;
377
468
  let assistantError: string | undefined;
@@ -410,19 +501,43 @@ async function runSingleAttempt(
410
501
  }
411
502
  };
412
503
 
413
- const detachForIntercom = () => {
414
- detached = true;
415
- processClosed = true;
416
- result.detached = true;
417
- result.detachedReason = "intercom coordination";
418
- progress.status = "detached";
419
- progress.durationMs = Date.now() - startTime;
420
- result.progressSummary = {
421
- toolCount: progress.toolCount,
422
- tokens: progress.tokens,
423
- durationMs: progress.durationMs,
504
+ const detachForeground = (reason: string): boolean => {
505
+ if (detached || processClosed || lifecycleFinished || options.signal?.aborted) return false;
506
+ const receiptProgress = snapshotProgress(progress);
507
+ receiptProgress.status = "detached";
508
+ receiptProgress.durationMs = Date.now() - startTime;
509
+ const receipt = snapshotResult(result, receiptProgress);
510
+ receipt.exitCode = -2;
511
+ receipt.detached = true;
512
+ receipt.detachedReason = reason;
513
+ receipt.finalOutput = reason === "intercom coordination"
514
+ ? "Detached for intercom coordination before task completion."
515
+ : reason === "user request"
516
+ ? "Detached at user request before task completion."
517
+ : `Detached for ${reason} before task completion.`;
518
+ receipt.outputMode = options.outputMode ?? "inline";
519
+ if (options.outputPath) {
520
+ receipt.outputSaveError = reason === "user request"
521
+ ? "Output file was not finalized because the subagent detached at user request."
522
+ : `Output file was not finalized because the subagent detached for ${reason}.`;
523
+ }
524
+ receipt.progressSummary = {
525
+ toolCount: receiptProgress.toolCount,
526
+ tokens: receiptProgress.tokens,
527
+ durationMs: receiptProgress.durationMs,
424
528
  };
425
- finish(-2);
529
+ // The attempt is not detached until the outer coordinator has accepted and
530
+ // synchronously published the receipt. A persistence/callback failure must
531
+ // leave this attempt attached and abortable rather than orphaning it.
532
+ let accepted = false;
533
+ try {
534
+ accepted = options.onDetachReceipt?.(receipt) === true;
535
+ } catch {
536
+ return false;
537
+ }
538
+ if (!accepted) return false;
539
+ detached = true;
540
+ return true;
426
541
  };
427
542
 
428
543
  // If the child emits a terminal assistant stop but never exits,
@@ -463,9 +578,9 @@ async function runSingleAttempt(
463
578
  armWatchdogTail();
464
579
  return;
465
580
  }
466
- if (childExited || finalDrainTimer || settled || processClosed || detached) return;
581
+ if (childExited || finalDrainTimer || lifecycleFinished || processClosed) return;
467
582
  finalDrainTimer = setTimeout(() => {
468
- if (settled || processClosed || detached) return;
583
+ if (lifecycleFinished || processClosed) return;
469
584
  const termSent = trySignalChild(proc, "SIGTERM");
470
585
  if (!termSent) return;
471
586
  forcedTerminationSignal = true;
@@ -473,7 +588,7 @@ async function runSingleAttempt(
473
588
  result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
474
589
  }
475
590
  finalHardKillTimer = setTimeout(() => {
476
- if (settled || processClosed || detached) return;
591
+ if (lifecycleFinished || processClosed) return;
477
592
  forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
478
593
  }, HARD_KILL_MS);
479
594
  finalHardKillTimer.unref?.();
@@ -481,7 +596,7 @@ async function runSingleAttempt(
481
596
  finalDrainTimer.unref?.();
482
597
  };
483
598
  function armWatchdogTail(): void {
484
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled || processClosed || detached) return;
599
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || processClosed) return;
485
600
  watchdogTailTimer = setTimeout(() => {
486
601
  watchdogTailTimer = undefined;
487
602
  updateChildWatchdogState({
@@ -507,7 +622,7 @@ async function runSingleAttempt(
507
622
  };
508
623
 
509
624
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
510
- if (!options.allowIntercomDetach || detached || processClosed) return;
625
+ if (!options.allowIntercomDetach || processClosed) return;
511
626
  if (!payload || typeof payload !== "object") return;
512
627
  const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
513
628
  const requestId = event.requestId;
@@ -518,13 +633,13 @@ async function runSingleAttempt(
518
633
  if (typeof event.agent === "string" && event.agent !== agent.name) return;
519
634
  if (typeof event.childIndex === "number" && event.childIndex !== (options.index ?? 0)) return;
520
635
  } else if (!intercomStarted) return;
521
- options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true, runId: options.runId, agent: agent.name, childIndex: options.index ?? 0 });
522
- detachForIntercom();
636
+ const accepted = detachForeground("intercom coordination");
637
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted, runId: options.runId, agent: agent.name, childIndex: options.index ?? 0 });
523
638
  });
524
639
 
525
640
  const finish = (code: number) => {
526
- if (settled) return;
527
- settled = true;
641
+ if (lifecycleFinished) return;
642
+ lifecycleFinished = true;
528
643
  clearFinalDrainTimers();
529
644
  clearWatchdogTailTimer();
530
645
  clearStdioGuard();
@@ -609,7 +724,7 @@ async function runSingleAttempt(
609
724
  };
610
725
  const requestTurnBudgetAbort = (turnCount: number) => {
611
726
  const budget = options.turnBudget;
612
- if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || settled || detached) return;
727
+ if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || lifecycleFinished) return;
613
728
  const message = turnBudgetExceededMessage(budget, turnCount);
614
729
  result.turnBudgetExceeded = true;
615
730
  result.wrapUpRequested = true;
@@ -622,12 +737,12 @@ async function runSingleAttempt(
622
737
  fireUpdate();
623
738
  trySignalChild(proc, "SIGINT");
624
739
  turnBudgetTerminationTimer = setTimeout(() => {
625
- if (processClosed || settled || detached || result.timedOut) return;
740
+ if (processClosed || lifecycleFinished || result.timedOut) return;
626
741
  trySignalChild(proc, "SIGTERM");
627
742
  }, 1000);
628
743
  turnBudgetTerminationTimer.unref?.();
629
744
  turnBudgetHardKillTimer = setTimeout(() => {
630
- if (processClosed || settled || detached || result.timedOut) return;
745
+ if (processClosed || lifecycleFinished || result.timedOut) return;
631
746
  trySignalChild(proc, "SIGKILL");
632
747
  }, 4000);
633
748
  turnBudgetHardKillTimer.unref?.();
@@ -683,7 +798,7 @@ async function runSingleAttempt(
683
798
  const emitUpdateSnapshot = (text: string) => {
684
799
  if (!options.onUpdate || processClosed) return;
685
800
  const progressSnapshot = snapshotProgress(progress);
686
- const resultSnapshot = snapshotResult(result, progressSnapshot);
801
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
687
802
  const controlEvents = drainPendingControlEvents();
688
803
  options.onUpdate({
689
804
  content: [{ type: "text", text }],
@@ -703,6 +818,7 @@ async function runSingleAttempt(
703
818
  emitUpdateSnapshot(output || "(running...)");
704
819
  };
705
820
 
821
+ const rawStdoutTail = createBoundedByteTail();
706
822
  const processLine = (line: string) => {
707
823
  if (!line.trim()) return;
708
824
  jsonlWriter.writeLine(line);
@@ -710,6 +826,7 @@ async function runSingleAttempt(
710
826
  try {
711
827
  evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
712
828
  } catch {
829
+ rawStdoutTail.push(`${line}\n`);
713
830
  shared.transcriptWriter?.writeStdoutLine(line);
714
831
  // Non-JSON stdout lines are expected; only structured events are parsed.
715
832
  return;
@@ -805,8 +922,13 @@ async function runSingleAttempt(
805
922
  result.usage.cacheWrite += u.cacheWrite || 0;
806
923
  result.usage.cost += u.cost?.total || 0;
807
924
  progress.tokens = result.usage.input + result.usage.output;
925
+ progress.inputTokens = result.usage.input;
926
+ progress.outputTokens = result.usage.output;
927
+ }
928
+ if (evt.message.model) {
929
+ progress.model = evt.message.model;
930
+ if (!result.model) result.model = evt.message.model;
808
931
  }
809
- if (!result.model && evt.message.model) result.model = evt.message.model;
810
932
  if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
811
933
  const assistantText = extractTextFromContent(evt.message.content);
812
934
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
@@ -857,7 +979,7 @@ async function runSingleAttempt(
857
979
 
858
980
  if (controlConfig.enabled) {
859
981
  activityTimer = setInterval(() => {
860
- if (processClosed || settled || detached) return;
982
+ if (processClosed || lifecycleFinished) return;
861
983
  const now = Date.now();
862
984
  if (updateActivityState(now)) {
863
985
  progress.durationMs = now - startTime;
@@ -869,7 +991,7 @@ async function runSingleAttempt(
869
991
 
870
992
  if (attemptTimeout) {
871
993
  timeoutTimer = setTimeout(() => {
872
- if (processClosed || settled || detached || interruptedByControl) return;
994
+ if (processClosed || lifecycleFinished || interruptedByControl) return;
873
995
  result.timedOut = true;
874
996
  result.error = attemptTimeout.message;
875
997
  result.finalOutput = attemptTimeout.message;
@@ -879,12 +1001,12 @@ async function runSingleAttempt(
879
1001
  fireUpdate();
880
1002
  trySignalChild(proc, "SIGINT");
881
1003
  timeoutTerminationTimer = setTimeout(() => {
882
- if (processClosed || settled || detached) return;
1004
+ if (processClosed || lifecycleFinished) return;
883
1005
  trySignalChild(proc, "SIGTERM");
884
1006
  }, 1000);
885
1007
  timeoutTerminationTimer.unref?.();
886
1008
  timeoutHardKillTimer = setTimeout(() => {
887
- if (processClosed || settled || detached) return;
1009
+ if (processClosed || lifecycleFinished) return;
888
1010
  trySignalChild(proc, "SIGKILL");
889
1011
  }, 4000);
890
1012
  timeoutHardKillTimer.unref?.();
@@ -928,76 +1050,46 @@ async function runSingleAttempt(
928
1050
  clearFinalDrainTimers();
929
1051
  });
930
1052
  proc.on("close", (code, signal) => {
1053
+ if (lifecycleFinished) return;
1054
+ processClosed = true;
931
1055
  clearFinalDrainTimers();
932
1056
  clearStdioGuard();
933
1057
  void jsonlWriter.close().catch(() => {
934
1058
  // JSONL artifact flush is best effort.
935
1059
  });
936
1060
  const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
1061
+ result.runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
937
1062
  cleanupTempDir(tempDir);
938
1063
  stdoutReader.end();
939
1064
  stderrReader.end();
940
1065
  const stderr = stderrTail.text();
1066
+ const rawStdout = rawStdoutTail.text();
941
1067
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
942
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1068
+ const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1069
+ if (signal) result.processSignal = signal;
1070
+ if (!closeError && isUnexplainedProcessSignal({
1071
+ processSignal: signal,
1072
+ interrupted: result.interrupted,
1073
+ timedOut: result.timedOut,
1074
+ stopped: result.stopped,
1075
+ turnBudgetExceeded: result.turnBudgetExceeded,
1076
+ forcedDrainAfterFinalSuccess,
1077
+ })) {
1078
+ closeError = formatProcessSignalError(signal!);
1079
+ }
1080
+ if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1081
+ closeError = rawStdout.trim();
1082
+ }
943
1083
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
944
1084
  closeError = stderr.trim();
945
1085
  }
946
1086
  const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
947
- if (detached) {
948
- const recoveredProgress = snapshotProgress(progress);
949
- const recoveredResult = snapshotResult(result, recoveredProgress);
950
- if (!recoveredResult.error && closeError) recoveredResult.error = closeError;
951
- recoveredResult.exitCode = recoveredResult.error && finalCode === 0 ? 1 : finalCode;
952
- recoveredProgress.status = recoveredResult.exitCode === 0 ? "completed" : "failed";
953
- recoveredProgress.durationMs = Date.now() - startTime;
954
- if (recoveredResult.error) recoveredProgress.error = recoveredResult.error;
955
- recoveredResult.progressSummary = {
956
- toolCount: recoveredProgress.toolCount,
957
- tokens: recoveredProgress.tokens,
958
- durationMs: recoveredProgress.durationMs,
959
- };
960
- const acceptanceOutput = getFinalOutput(recoveredResult.messages ?? []).trim()
961
- || recoveredResult.error
962
- || recoveredResult.finalOutput
963
- || "Detached child exited without final output.";
964
- acceptanceOutputByResult.set(recoveredResult, acceptanceOutput);
965
- let fullOutput = stripAcceptanceReport(acceptanceOutput);
966
- fullOutput = fullOutput.trim() || recoveredResult.error || recoveredResult.finalOutput || "Detached child exited without final output.";
967
- recoveredResult.outputMode = options.outputMode ?? "inline";
968
- if (options.outputPath && recoveredResult.exitCode === 0) {
969
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
970
- fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
971
- recoveredResult.savedOutputPath = resolvedOutput.savedPath;
972
- recoveredResult.outputSaveError = resolvedOutput.saveError;
973
- if (resolvedOutput.savedPath) {
974
- recoveredResult.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
975
- } else {
976
- recoveredResult.exitCode = 1;
977
- recoveredResult.error = `Output file was not finalized after detached child exit: ${resolvedOutput.saveError ?? options.outputPath}`;
978
- recoveredProgress.status = "failed";
979
- recoveredProgress.error = recoveredResult.error;
980
- }
981
- }
982
- recoveredResult.finalOutput = options.outputMode === "file-only" && recoveredResult.savedOutputPath && recoveredResult.outputReference
983
- ? recoveredResult.outputReference.message
984
- : fullOutput;
985
- if (recoveredResult.artifactPaths && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {
986
- try {
987
- writeArtifact(recoveredResult.artifactPaths.outputPath, fullOutput);
988
- } catch {
989
- // Detached children may outlive test/temp cleanup; recovered status is best-effort.
990
- }
991
- }
992
- options.onDetachedExit?.(recoveredResult);
993
- finish(-2);
994
- return;
995
- }
996
1087
  if (!result.error && closeError) result.error = closeError;
997
- processClosed = true;
998
1088
  finish(finalCode);
999
1089
  });
1000
1090
  proc.on("error", (error) => {
1091
+ if (lifecycleFinished) return;
1092
+ processClosed = true;
1001
1093
  clearFinalDrainTimers();
1002
1094
  clearStdioGuard();
1003
1095
  void jsonlWriter.close().catch(() => {
@@ -1014,7 +1106,7 @@ async function runSingleAttempt(
1014
1106
 
1015
1107
  if (options.signal) {
1016
1108
  const kill = () => {
1017
- if (processClosed || detached) return;
1109
+ if (processClosed || lifecycleFinished) return;
1018
1110
  proc.kill("SIGTERM");
1019
1111
  setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
1020
1112
  };
@@ -1027,7 +1119,7 @@ async function runSingleAttempt(
1027
1119
 
1028
1120
  if (options.interruptSignal) {
1029
1121
  const interrupt = () => {
1030
- if (processClosed || detached || settled) return;
1122
+ if (processClosed || lifecycleFinished) return;
1031
1123
  if (result.timedOut) return;
1032
1124
  interruptedByControl = true;
1033
1125
  clearTimeoutTimers();
@@ -1039,7 +1131,7 @@ async function runSingleAttempt(
1039
1131
  fireUpdate();
1040
1132
  trySignalChild(proc, "SIGINT");
1041
1133
  setTimeout(() => {
1042
- if (settled || processClosed || detached) return;
1134
+ if (lifecycleFinished || processClosed) return;
1043
1135
  trySignalChild(proc, "SIGTERM");
1044
1136
  }, 1000).unref?.();
1045
1137
  };
@@ -1049,6 +1141,16 @@ async function runSingleAttempt(
1049
1141
  removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
1050
1142
  }
1051
1143
  }
1144
+
1145
+ // Publish only after every callback and cleanup guard captured by detach or
1146
+ // later lifecycle events has initialized. Consumers may invoke synchronously.
1147
+ try {
1148
+ options.onDetachReady?.((reason = "user request") => detachForeground(reason));
1149
+ } catch (error) {
1150
+ // A consumer callback is advisory. Keep the child attached and observable
1151
+ // rather than rejecting this attempt and orphaning its live process.
1152
+ appendRecentOutput(progress, [`Foreground detach callback failed: ${error instanceof Error ? error.message : String(error)}`]);
1153
+ }
1052
1154
  });
1053
1155
  result.exitCode = exitCode;
1054
1156
  if (interruptedByControl) {
@@ -1066,16 +1168,6 @@ async function runSingleAttempt(
1066
1168
  };
1067
1169
  return result;
1068
1170
  }
1069
- if (result.detached) {
1070
- result.exitCode = -2;
1071
- result.finalOutput = "Detached for intercom coordination before task completion.";
1072
- result.outputMode = options.outputMode ?? "inline";
1073
- if (options.outputPath) {
1074
- result.outputSaveError = "Output file was not finalized because the subagent detached for intercom coordination.";
1075
- }
1076
- return result;
1077
- }
1078
-
1079
1171
  if (result.error && result.exitCode === 0) {
1080
1172
  result.exitCode = 1;
1081
1173
  }
@@ -1137,6 +1229,7 @@ async function runSingleAttempt(
1137
1229
 
1138
1230
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1139
1231
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
1232
+ result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1140
1233
  if (result.timedOut) {
1141
1234
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1142
1235
  fullOutput = fullOutput.trim()
@@ -1196,6 +1289,7 @@ async function runSingleAttempt(
1196
1289
  result.outputSaveError = resolvedOutput.saveError;
1197
1290
  if (resolvedOutput.savedPath) {
1198
1291
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1292
+ if (result.outputState === "absent") result.outputState = "unknown";
1199
1293
  }
1200
1294
  }
1201
1295
  artifactOutputByResult.set(result, fullOutput);
@@ -1208,7 +1302,7 @@ async function runSingleAttempt(
1208
1302
  if (options.onUpdate) {
1209
1303
  const finalText = result.finalOutput || result.error || "(no output)";
1210
1304
  const progressSnapshot = snapshotProgress(progress);
1211
- const resultSnapshot = snapshotResult(result, progressSnapshot);
1305
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
1212
1306
  options.onUpdate({
1213
1307
  content: [{ type: "text", text: finalText }],
1214
1308
  details: {
@@ -1225,7 +1319,7 @@ async function runSingleAttempt(
1225
1319
  /**
1226
1320
  * Run a subagent synchronously (blocking until complete)
1227
1321
  */
1228
- export async function runSync(
1322
+ async function runSyncCompletion(
1229
1323
  runtimeCwd: string,
1230
1324
  agents: AgentConfig[],
1231
1325
  agentName: string,
@@ -1234,11 +1328,12 @@ export async function runSync(
1234
1328
  ): Promise<SingleResult> {
1235
1329
  options = {
1236
1330
  ...options,
1237
- capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
1331
+ capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1238
1332
  };
1239
1333
  const agent = agents.find((a) => a.name === agentName);
1240
1334
  if (!agent) {
1241
1335
  return withRunContext({
1336
+ index: options.index ?? 0,
1242
1337
  agent: agentName,
1243
1338
  task,
1244
1339
  exitCode: 1,
@@ -1247,9 +1342,23 @@ export async function runSync(
1247
1342
  error: `Unknown agent: ${agentName}`,
1248
1343
  }, options.context);
1249
1344
  }
1345
+ try {
1346
+ assertAgentAllowedByCapabilityCeiling(agent.name, options.capabilityCeiling);
1347
+ } catch (error) {
1348
+ return withRunContext({
1349
+ agent: agent.name,
1350
+ task,
1351
+ exitCode: 1,
1352
+ messages: [],
1353
+ usage: emptyUsage(),
1354
+ error: error instanceof Error ? error.message : String(error),
1355
+ ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
1356
+ }, options.context);
1357
+ }
1250
1358
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1251
1359
  if (outputModeValidationError) {
1252
1360
  return withRunContext({
1361
+ index: options.index ?? 0,
1253
1362
  agent: agentName,
1254
1363
  task,
1255
1364
  exitCode: 1,
@@ -1286,6 +1395,7 @@ export async function runSync(
1286
1395
  );
1287
1396
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1288
1397
  return withRunContext({
1398
+ index: options.index ?? 0,
1289
1399
  agent: agentName,
1290
1400
  task,
1291
1401
  exitCode: 1,
@@ -1345,121 +1455,149 @@ export async function runSync(
1345
1455
  }
1346
1456
 
1347
1457
  const persistResultMetadata = (target: SingleResult): void => {
1348
- if (!artifactPathsResult || options.artifactConfig?.enabled === false || options.artifactConfig?.includeMetadata === false) return;
1349
- writeMetadata(artifactPathsResult.metadataPath, {
1458
+ persistSingleResultMetadata({
1459
+ metadataPath: artifactPathsResult?.metadataPath,
1460
+ enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
1350
1461
  runId: options.runId,
1351
1462
  agent: agentName,
1352
1463
  task,
1353
- exitCode: target.exitCode,
1354
- usage: target.usage,
1355
- model: target.model,
1356
- attemptedModels: target.attemptedModels,
1357
- modelAttempts: target.modelAttempts,
1358
- durationMs: target.progressSummary?.durationMs,
1359
- toolCount: target.progressSummary?.toolCount,
1360
- error: target.error,
1361
- agentContract: target.agentContract,
1362
- launchContractDigest: target.launchContractDigest,
1363
- execution: target.execution,
1364
- acceptance: target.acceptance,
1365
- capabilityCeiling: target.capabilityCeiling,
1366
- capabilityAudit: target.capabilityAudit,
1367
- review: target.review,
1368
- effects: target.effects,
1369
- ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
1370
- transcriptError: target.transcriptError,
1371
- skills: target.skills,
1372
- skillsWarning: target.skillsWarning,
1373
- timestamp: Date.now(),
1464
+ result: target,
1374
1465
  });
1375
1466
  };
1376
1467
 
1377
- const detachedAwareOptions: RunSyncOptions = options.onDetachedExit
1378
- ? {
1379
- ...options,
1380
- onDetachedExit: (recoveredResult) => {
1381
- void (async () => {
1382
- const childWrittenOutput = options.outputPath
1383
- ? extractChildWrittenOutput(recoveredResult.messages, options.outputPath, options.cwd ?? runtimeCwd)
1384
- : undefined;
1385
- recoveredResult.acceptance = await evaluateAcceptance({
1386
- acceptance: effectiveAcceptance,
1387
- output: acceptanceOutputByResult.get(recoveredResult) ?? recoveredResult.finalOutput ?? "",
1388
- fileOutput: childWrittenOutput !== undefined && options.outputPath
1389
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1390
- : undefined,
1391
- cwd: options.cwd ?? runtimeCwd,
1392
- reportOptional: isAgentContractV1(options.agentContract),
1393
- });
1394
- const acceptanceFailure = acceptanceFailureMessage(recoveredResult.acceptance);
1395
- stripAcceptanceReportsFromMessages(recoveredResult.messages);
1396
- if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0 && !isAgentContractV1(options.agentContract)) {
1397
- recoveredResult.exitCode = 1;
1398
- recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\n${acceptanceFailure}` : acceptanceFailure;
1399
- if (recoveredResult.progress) {
1400
- recoveredResult.progress.status = "failed";
1401
- recoveredResult.progress.error = recoveredResult.error;
1468
+ let intercomDetached = false;
1469
+ let detachedReason: string | undefined;
1470
+ const attemptOptions: RunSyncOptions = {
1471
+ ...options,
1472
+ onDetachReceipt: (receipt) => {
1473
+ receipt.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
1474
+ try {
1475
+ persistResultMetadata(receipt);
1476
+ } catch (error) {
1477
+ receipt.metadataSaveError = error instanceof Error ? error.message : String(error);
1478
+ }
1479
+ const accepted = options.onDetachReceipt?.(receipt) === true;
1480
+ if (accepted) {
1481
+ detachedReason = receipt.detachedReason;
1482
+ if (receipt.detachedReason === "intercom coordination") intercomDetached = true;
1483
+ }
1484
+ return accepted;
1485
+ },
1486
+ };
1487
+ let lastResult: SingleResult | undefined;
1488
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1489
+ modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1490
+ const candidate = modelsToTry[modelIndex];
1491
+ for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1492
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1493
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
1494
+ sessionEnabled,
1495
+ systemPrompt,
1496
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1497
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1498
+ jsonlPath,
1499
+ artifactPaths: artifactPathsResult,
1500
+ transcriptWriter,
1501
+ attemptNotes,
1502
+ modelCandidates: candidates
1503
+ .map((modelCandidate) => applyThinkingSuffix(modelCandidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined))
1504
+ .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
1505
+ outputSnapshot,
1506
+ originalTask: task,
1507
+ });
1508
+ lastResult = result;
1509
+ if (startupAttemptIndex === 0) {
1510
+ if (result.model) attemptedModels.push(result.model);
1511
+ else if (candidate) attemptedModels.push(candidate);
1512
+ }
1513
+ sumUsage(aggregateUsage, result.usage);
1514
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
1515
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
1516
+ const attemptSucceeded = result.exitCode === 0 && !result.error;
1517
+ const attempt: ModelAttempt = {
1518
+ model: result.model ?? candidate ?? agent.model ?? "default",
1519
+ success: attemptSucceeded,
1520
+ exitCode: result.exitCode,
1521
+ error: result.error,
1522
+ usage: { ...result.usage },
1523
+ };
1524
+ modelAttempts.push(attempt);
1525
+ // Preserve the legacy intercom handoff contract: once this logical run has
1526
+ // been handed to a supervisor, terminating that attempt must not launch a
1527
+ // startup retry or model fallback. Explicit user detach retains fallback.
1528
+ if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
1529
+ if (attemptSucceeded) break modelAttemptsLoop;
1530
+
1531
+ const startupFailure = isRetryableSubagentStartupFailure({
1532
+ exitCode: result.exitCode,
1533
+ error: result.error,
1534
+ finalOutput: result.finalOutput,
1535
+ messageCount: result.messages?.length ?? 0,
1536
+ toolCount: result.progressSummary?.toolCount ?? 0,
1537
+ usage: result.usage,
1538
+ durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
1539
+ protocolError: result.protocolError,
1540
+ processSignal: result.processSignal,
1541
+ detached: result.detached,
1542
+ interrupted: result.interrupted,
1543
+ timedOut: result.timedOut,
1544
+ stopped: result.stopped,
1545
+ turnBudgetExceeded: result.turnBudgetExceeded,
1546
+ });
1547
+ const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1548
+ if (startupFailure && retryDelayMs !== undefined) {
1549
+ const retryNote = formatSubagentStartupRetryNote({
1550
+ model: attempt.model,
1551
+ attempt: startupAttemptIndex + 1,
1552
+ maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
1553
+ delayMs: retryDelayMs,
1554
+ });
1555
+ const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
1556
+ if (!shouldRetry) {
1557
+ if (options.interruptSignal?.aborted) {
1558
+ result.exitCode = 0;
1559
+ result.interrupted = true;
1560
+ result.error = undefined;
1561
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
1562
+ if (result.progress) {
1563
+ result.progress.status = "running";
1564
+ result.progress.error = undefined;
1402
1565
  }
1566
+ } else {
1567
+ const cancellationError = "Subagent startup retry cancelled before relaunch.";
1568
+ result.error = cancellationError;
1569
+ result.finalOutput = cancellationError;
1570
+ attempt.error = cancellationError;
1571
+ if (result.progress) result.progress.error = cancellationError;
1403
1572
  }
1404
- if (isAgentContractV1(options.agentContract)) attachContractProjections(recoveredResult);
1405
- persistResultMetadata(recoveredResult);
1406
- options.onDetachedExit?.(recoveredResult);
1407
- })().catch((error) => {
1408
- const message = error instanceof Error ? error.message : String(error);
1409
- recoveredResult.exitCode = 1;
1410
- recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\nAcceptance evaluation failed: ${message}` : `Acceptance evaluation failed: ${message}`;
1411
- options.onDetachedExit?.(recoveredResult);
1573
+ break modelAttemptsLoop;
1574
+ }
1575
+ attempt.error = retryNote;
1576
+ attemptNotes.push(retryNote);
1577
+ continue;
1578
+ }
1579
+ if (startupFailure) {
1580
+ const startupError = formatSubagentStartupRetryExhaustedError({
1581
+ model: attempt.model,
1582
+ attempts: startupAttemptIndex + 1,
1412
1583
  });
1413
- },
1414
- }
1415
- : options;
1416
-
1417
- let lastResult: SingleResult | undefined;
1418
- const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1419
- for (let i = 0; i < modelsToTry.length; i++) {
1420
- const candidate = modelsToTry[i];
1421
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1422
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
1423
- sessionEnabled,
1424
- systemPrompt,
1425
- resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1426
- skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1427
- jsonlPath,
1428
- artifactPaths: artifactPathsResult,
1429
- transcriptWriter,
1430
- attemptNotes,
1431
- modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1432
- outputSnapshot,
1433
- originalTask: task,
1434
- });
1435
- lastResult = result;
1436
- if (result.model) attemptedModels.push(result.model);
1437
- else if (candidate) attemptedModels.push(candidate);
1438
- sumUsage(aggregateUsage, result.usage);
1439
- totalToolCount += result.progressSummary?.toolCount ?? 0;
1440
- totalDurationMs += result.progressSummary?.durationMs ?? 0;
1441
- const attemptSucceeded = result.exitCode === 0 && !result.error;
1442
- const attempt: ModelAttempt = {
1443
- model: result.model ?? candidate ?? agent.model ?? "default",
1444
- success: attemptSucceeded,
1445
- exitCode: result.exitCode,
1446
- error: result.error,
1447
- usage: { ...result.usage },
1448
- };
1449
- modelAttempts.push(attempt);
1450
- if (result.detached || result.timedOut || result.turnBudgetExceeded) {
1451
- break;
1452
- }
1453
- if (attemptSucceeded) {
1454
- break;
1455
- }
1456
- if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
1584
+ result.error = startupError;
1585
+ result.finalOutput = startupError;
1586
+ if (result.progress) {
1587
+ result.progress.error = startupError;
1588
+ result.progress.status = "failed";
1589
+ }
1590
+ attempt.error = startupError;
1591
+ break modelAttemptsLoop;
1592
+ }
1593
+ if (!isRetryableModelFailure(result.error) || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
1594
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
1457
1595
  break;
1458
1596
  }
1459
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1460
1597
  }
1461
1598
 
1462
1599
  const result = withRunContext(lastResult ?? {
1600
+ index: options.index ?? 0,
1463
1601
  agent: agentName,
1464
1602
  task,
1465
1603
  exitCode: 1,
@@ -1476,35 +1614,33 @@ export async function runSync(
1476
1614
  tokens: aggregateUsage.input + aggregateUsage.output,
1477
1615
  durationMs: totalDurationMs,
1478
1616
  };
1479
- if (attemptNotes.length > 0 && result.progress) {
1480
- result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
1481
- if (result.progress.recentOutput.length > 50) {
1482
- result.progress.recentOutput.splice(50);
1483
- }
1484
- }
1485
-
1486
1617
  if (transcriptWriter) result.transcriptPath = artifactPathsResult?.transcriptPath;
1487
1618
  if (transcriptWriter?.getError()) result.transcriptError = transcriptWriter.getError();
1488
1619
 
1489
- if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1490
- result.artifactPaths = artifactPathsResult;
1491
- if (options.artifactConfig?.includeOutput !== false) {
1492
- writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1493
- output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1494
- error: result.error,
1495
- transcriptPath: result.transcriptPath,
1496
- metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1497
- }));
1498
- }
1499
- if (options.maxOutput) {
1620
+ try {
1621
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1622
+ result.artifactPaths = artifactPathsResult;
1623
+ if (options.artifactConfig?.includeOutput !== false) {
1624
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1625
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1626
+ error: result.error,
1627
+ transcriptPath: result.transcriptPath,
1628
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1629
+ }));
1630
+ }
1631
+ if (options.maxOutput) {
1632
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1633
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
1634
+ if (truncationResult.truncated) result.truncation = truncationResult;
1635
+ }
1636
+ } else if (options.maxOutput) {
1500
1637
  const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1501
- const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
1638
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
1502
1639
  if (truncationResult.truncated) result.truncation = truncationResult;
1503
1640
  }
1504
- } else if (options.maxOutput) {
1505
- const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1506
- const truncationResult = truncateOutput(result.finalOutput ?? "", config);
1507
- if (truncationResult.truncated) result.truncation = truncationResult;
1641
+ } catch (error) {
1642
+ const message = `Artifact output post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
1643
+ result.outputSaveError = result.outputSaveError ? `${result.outputSaveError}\n${message}` : message;
1508
1644
  }
1509
1645
 
1510
1646
  if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
@@ -1517,28 +1653,36 @@ export async function runSync(
1517
1653
  const childWrittenOutput = options.outputPath
1518
1654
  ? extractChildWrittenOutput(result.messages, options.outputPath, options.cwd ?? runtimeCwd)
1519
1655
  : undefined;
1520
- if (result.detached) {
1521
- result.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
1522
- } else if (result.stopped) {
1523
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
1524
- } else if (result.timedOut) {
1525
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
1526
- } else if (result.turnBudgetExceeded) {
1527
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
1528
- } else {
1529
- result.acceptance = await evaluateAcceptance({
1530
- acceptance: effectiveAcceptance,
1531
- output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
1532
- fileOutput: childWrittenOutput !== undefined && options.outputPath
1533
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1534
- : undefined,
1535
- cwd: options.cwd ?? runtimeCwd,
1536
- reportOptional: isAgentContractV1(options.agentContract),
1537
- });
1656
+ try {
1657
+ if (result.interrupted && detachedReason === "user request") {
1658
+ // Only an accepted user-detach receipt needs a non-rejecting terminal
1659
+ // ledger. Attached and background execution retain their baseline
1660
+ // acceptance behavior.
1661
+ result.acceptance = buildInterruptedAcceptanceLedger(effectiveAcceptance);
1662
+ } else if (result.stopped) {
1663
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
1664
+ } else if (result.timedOut) {
1665
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
1666
+ } else if (result.turnBudgetExceeded) {
1667
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
1668
+ } else {
1669
+ result.acceptance = await evaluateAcceptance({
1670
+ acceptance: effectiveAcceptance,
1671
+ output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
1672
+ fileOutput: childWrittenOutput !== undefined && options.outputPath
1673
+ ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1674
+ : undefined,
1675
+ cwd: options.cwd ?? runtimeCwd,
1676
+ reportOptional: isAgentContractV1(options.agentContract),
1677
+ });
1678
+ }
1679
+ } catch (error) {
1680
+ const message = `Acceptance evaluation failed: ${error instanceof Error ? error.message : String(error)}`;
1681
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "acceptance-evaluation", message });
1538
1682
  }
1539
1683
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1540
1684
  stripAcceptanceReportsFromMessages(result.messages);
1541
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1685
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1542
1686
  result.exitCode = 1;
1543
1687
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1544
1688
  if (result.progress) {
@@ -1547,7 +1691,143 @@ export async function runSync(
1547
1691
  }
1548
1692
  }
1549
1693
  if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1550
- persistResultMetadata(result);
1694
+ try {
1695
+ persistResultMetadata(result);
1696
+ } catch (error) {
1697
+ result.metadataSaveError = error instanceof Error ? error.message : String(error);
1698
+ }
1551
1699
 
1552
1700
  return result;
1553
1701
  }
1702
+
1703
+ /**
1704
+ * Runs the authoritative completion pipeline independently from the foreground
1705
+ * receipt. Detachment is same-runtime only: it does not adopt or daemonize work.
1706
+ */
1707
+ export async function runSync(
1708
+ runtimeCwd: string,
1709
+ agents: AgentConfig[],
1710
+ agentName: string,
1711
+ task: string,
1712
+ options: RunSyncOptions,
1713
+ ): Promise<SingleResult> {
1714
+ // Capture the strict contract before consumer-owned objects can be mutated
1715
+ // after a detached receipt is published.
1716
+ const strictContract = isAgentContractV1(options.agentContract);
1717
+ let detachedReason: string | undefined;
1718
+ let publishedReceipt: SingleResult | undefined;
1719
+ let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
1720
+ let detachReadyPublished = false;
1721
+ let resolveReceipt!: (result: SingleResult) => void;
1722
+ const receipt = new Promise<SingleResult>((resolve) => { resolveReceipt = resolve; });
1723
+ const originSignal = options.signal;
1724
+ const originController = new AbortController();
1725
+ const forwardOriginAbort = () => {
1726
+ if (!detachedReason) originController.abort(originSignal?.reason);
1727
+ };
1728
+ if (originSignal?.aborted) forwardOriginAbort();
1729
+ else originSignal?.addEventListener("abort", forwardOriginAbort, { once: true });
1730
+
1731
+ const completion = runSyncCompletion(runtimeCwd, agents, agentName, task, {
1732
+ ...options,
1733
+ signal: originController.signal,
1734
+ onDetachedExit: undefined,
1735
+ onDetachReceipt: (detachedReceipt) => {
1736
+ if (detachedReason || publishedReceipt) return false;
1737
+ // Keep the authoritative result, fallback snapshot, and caller receipt
1738
+ // separately owned while the completion pipeline remains live.
1739
+ publishedReceipt = structuredClone(detachedReceipt);
1740
+ const callerReceipt = structuredClone(detachedReceipt);
1741
+ // A strict contract was already validated before detach; normalize private
1742
+ // and caller snapshots to the only safely known version.
1743
+ publishedReceipt.agentContract = strictContract ? { version: 1 } : undefined;
1744
+ callerReceipt.agentContract = strictContract ? { version: 1 } : undefined;
1745
+ detachedReason = detachedReceipt.detachedReason ?? "user request";
1746
+ resolveReceipt(callerReceipt);
1747
+ return true;
1748
+ },
1749
+ onDetachReady: (detachAttempt) => {
1750
+ activeDetachAttempt = detachAttempt;
1751
+ if (detachReadyPublished) return;
1752
+ detachReadyPublished = true;
1753
+ options.onDetachReady?.((reason = "user request") => {
1754
+ if (detachedReason || originController.signal.aborted) return false;
1755
+ return activeDetachAttempt?.(reason) ?? false;
1756
+ });
1757
+ },
1758
+ });
1759
+
1760
+ const authoritativeCompletion = completion.catch((error: unknown) => {
1761
+ if (!publishedReceipt || !detachedReason) throw error;
1762
+ const message = error instanceof Error ? error.message : String(error);
1763
+ const failureMessage = `Detached completion pipeline failed after receipt: ${message}`;
1764
+ const failedProgress: AgentProgress = publishedReceipt.progress
1765
+ ? { ...publishedReceipt.progress, status: "failed", error: failureMessage }
1766
+ : {
1767
+ index: options.index ?? 0,
1768
+ agent: publishedReceipt.agent,
1769
+ status: "failed",
1770
+ task: publishedReceipt.task,
1771
+ recentTools: [],
1772
+ recentOutput: [],
1773
+ toolCount: publishedReceipt.progressSummary?.toolCount ?? 0,
1774
+ tokens: publishedReceipt.progressSummary?.tokens ?? 0,
1775
+ durationMs: publishedReceipt.progressSummary?.durationMs ?? 0,
1776
+ error: failureMessage,
1777
+ };
1778
+ const failedResult: SingleResult = {
1779
+ ...publishedReceipt,
1780
+ detached: undefined,
1781
+ detachedReason,
1782
+ exitCode: 1,
1783
+ error: failureMessage,
1784
+ finalOutput: failureMessage,
1785
+ progress: failedProgress,
1786
+ progressSummary: {
1787
+ toolCount: failedProgress.toolCount,
1788
+ tokens: failedProgress.tokens,
1789
+ durationMs: failedProgress.durationMs,
1790
+ },
1791
+ acceptance: publishedReceipt.acceptance
1792
+ ? buildSkippedAcceptanceLedger(publishedReceipt.acceptance.effectiveAcceptance, { id: "completion-pipeline", message: failureMessage })
1793
+ : undefined,
1794
+ };
1795
+ if (strictContract) attachContractProjections(failedResult);
1796
+ try {
1797
+ // Replace the provisional detach receipt metadata with the authoritative
1798
+ // terminal failure. Persistence remains best-effort and cannot orphan work.
1799
+ persistSingleResultMetadata({
1800
+ metadataPath: failedResult.artifactPaths?.metadataPath,
1801
+ enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
1802
+ runId: options.runId,
1803
+ agent: failedResult.agent,
1804
+ task: failedResult.task,
1805
+ result: failedResult,
1806
+ });
1807
+ } catch (metadataError) {
1808
+ failedResult.metadataSaveError = metadataError instanceof Error ? metadataError.message : String(metadataError);
1809
+ }
1810
+ return failedResult;
1811
+ });
1812
+
1813
+ let terminalCallbackInvoked = false;
1814
+ void authoritativeCompletion.then((terminalResult) => {
1815
+ if (!detachedReason || terminalCallbackInvoked) return;
1816
+ terminalCallbackInvoked = true;
1817
+ terminalResult.detached = undefined;
1818
+ terminalResult.detachedReason = detachedReason;
1819
+ try {
1820
+ options.onDetachedExit?.(terminalResult);
1821
+ } catch {
1822
+ // The authoritative result has settled. Consumer callback failures are
1823
+ // contained here; each consumer owns cleanup through its own finally block.
1824
+ }
1825
+ }).catch(() => {
1826
+ // Attached completion rejection remains observable through the returned
1827
+ // promise without becoming an unhandled side-channel rejection.
1828
+ }).finally(() => {
1829
+ originSignal?.removeEventListener("abort", forwardOriginAbort);
1830
+ });
1831
+
1832
+ return Promise.race([authoritativeCompletion, receipt]);
1833
+ }