@selesai/code 0.5.28 → 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 (307) 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/main.d.ts.map +1 -1
  247. package/dist/main.js +43 -0
  248. package/dist/main.js.map +1 -1
  249. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  250. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  251. package/dist/modes/interactive/components/custom-message.js +10 -2
  252. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  253. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  254. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/extension-editor.js +16 -46
  256. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  257. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/model-selector.js +4 -1
  259. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  260. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  261. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  262. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  263. package/dist/modes/interactive/external-editor.d.ts +12 -0
  264. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  265. package/dist/modes/interactive/external-editor.js +37 -0
  266. package/dist/modes/interactive/external-editor.js.map +1 -0
  267. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  268. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  269. package/dist/modes/interactive/interactive-mode.js +72 -64
  270. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  271. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  272. package/dist/modes/rpc/rpc-mode.js +14 -0
  273. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  274. package/dist/utils/clipboard.d.ts.map +1 -1
  275. package/dist/utils/clipboard.js +19 -8
  276. package/dist/utils/clipboard.js.map +1 -1
  277. package/dist/utils/version-check.d.ts.map +1 -1
  278. package/dist/utils/version-check.js +1 -1
  279. package/dist/utils/version-check.js.map +1 -1
  280. package/docs/compaction.md +1 -1
  281. package/docs/custom-provider.md +14 -5
  282. package/docs/environment-variables.md +86 -0
  283. package/docs/extensions.md +15 -5
  284. package/docs/index.md +1 -0
  285. package/docs/models.md +9 -2
  286. package/docs/providers.md +21 -2
  287. package/docs/rpc.md +23 -6
  288. package/docs/session-format.md +2 -0
  289. package/docs/settings.md +1 -1
  290. package/docs/usage.md +0 -13
  291. package/examples/extensions/custom-compaction.ts +5 -2
  292. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  293. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  294. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  295. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  296. package/examples/extensions/gondolin/package-lock.json +2 -2
  297. package/examples/extensions/gondolin/package.json +1 -1
  298. package/examples/extensions/handoff.ts +11 -3
  299. package/examples/extensions/message-renderer.ts +3 -3
  300. package/examples/extensions/sandbox/package-lock.json +2 -2
  301. package/examples/extensions/sandbox/package.json +1 -1
  302. package/examples/extensions/summarize.ts +5 -2
  303. package/examples/extensions/with-deps/package-lock.json +2 -2
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/examples/sdk/12-full-control.ts +3 -1
  306. package/package.json +15 -6
  307. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -81,6 +81,22 @@ interface ArtifactPaths {
81
81
  metadataPath?: string;
82
82
  }
83
83
 
84
+ interface LaunchResolvedExtensions {
85
+ version?: number;
86
+ source?: string;
87
+ disableAmbientExtensions?: boolean;
88
+ runtime?: string[];
89
+ configured?: string[];
90
+ effective?: string[];
91
+ }
92
+
93
+ interface RuntimeAcknowledgedExtensions {
94
+ version?: number;
95
+ source?: string;
96
+ ids?: string[];
97
+ omitted?: number;
98
+ }
99
+
84
100
  interface RunSyncResult {
85
101
  exitCode: number;
86
102
  agent: string;
@@ -99,6 +115,7 @@ interface RunSyncResult {
99
115
  transcriptPath?: string;
100
116
  transcriptError?: string;
101
117
  finalOutput?: string;
118
+ processSignal?: string | null;
102
119
  interrupted?: boolean;
103
120
  timedOut?: boolean;
104
121
  turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; exceededAtTurn?: number };
@@ -121,6 +138,8 @@ interface RunSyncResult {
121
138
  verifyRuns?: Array<{ status?: string }>;
122
139
  runtimeChecks?: Array<{ id?: string; status?: string; message?: string }>;
123
140
  };
141
+ launchResolvedExtensions?: LaunchResolvedExtensions;
142
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
124
143
  }
125
144
 
126
145
  interface MockPiCallRecord {
@@ -230,6 +249,7 @@ interface ExecutorModule {
230
249
  execute: (...args: unknown[]) => Promise<ExecutorToolResult>;
231
250
  executeDelegated: (...args: unknown[]) => Promise<ExecutorToolResult>;
232
251
  };
252
+ DEFAULT_FOREGROUND_TIMEOUT_MS?: number;
233
253
  }
234
254
 
235
255
  const execution = await tryImport<ExecutionModule>("./src/runs/foreground/execution.ts");
@@ -572,7 +592,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
572
592
  extensionEvents.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
573
593
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
574
594
  requestId: "registered-a",
575
- agent: "worker",
595
+ agent: "builder",
576
596
  task: "First registered delegated call",
577
597
  context: "fresh",
578
598
  cwd: tempDir,
@@ -581,7 +601,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
581
601
  extensionEvents.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, {
582
602
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
583
603
  requestId: "registered-b",
584
- agent: "reviewer",
604
+ agent: "commentator",
585
605
  task: "Second registered delegated call",
586
606
  context: "fresh",
587
607
  cwd: tempDir,
@@ -693,7 +713,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
693
713
  requestId: "registered-v2-a",
694
714
  ownerRunId: "owner-v2",
695
715
  nodeId: "node-a",
696
- agent: "worker",
716
+ agent: "builder",
697
717
  task: "Return literal JSON-looking text",
698
718
  context: "fresh",
699
719
  cwd: tempDir,
@@ -706,7 +726,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
706
726
  requestId: "registered-v2-b",
707
727
  ownerRunId: "owner-v2",
708
728
  nodeId: "node-b",
709
- agent: "reviewer",
729
+ agent: "commentator",
710
730
  task: "Run the second logical node",
711
731
  context: "fresh",
712
732
  cwd: tempDir,
@@ -745,6 +765,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
745
765
  assert.equal(response.ownerRunId, "owner-v2");
746
766
  assert.equal(response.model, "mock/test-model:high");
747
767
  assert.equal(response.thinking, "high");
768
+ assert.match(response.launchContractDigest ?? "", /^[0-9a-f]{64}$/);
748
769
  }
749
770
  const literalResponse = terminalResponses.find((response) => response.result?.kind === "text" && response.result.text === literalJsonText);
750
771
  assert.ok(literalResponse);
@@ -1049,6 +1070,58 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1049
1070
  assert.deepEqual(result.details?.totalCost, { inputTokens: 100, outputTokens: 50, costUsd: 0.001 });
1050
1071
  });
1051
1072
 
1073
+ it("blocks later foreground chain children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1074
+ mockPi.onCall({ output: "first result" });
1075
+ const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
1076
+
1077
+ const result = await executor.execute(
1078
+ "foreground-usage-budget",
1079
+ {
1080
+ chain: [
1081
+ { agent: "echo", task: "First task" },
1082
+ { agent: "second", task: "Second task" },
1083
+ ],
1084
+ usageBudget: { tokens: { hard: 10 } },
1085
+ },
1086
+ new AbortController().signal,
1087
+ undefined,
1088
+ makeMinimalCtx(tempDir),
1089
+ );
1090
+
1091
+ assert.equal(result.isError, true);
1092
+ assert.match(result.content[0]?.text ?? "", /Usage budget exhausted/);
1093
+ assert.equal(mockPi.callCount(), 1);
1094
+ assert.equal(result.details?.usageBudget?.exhausted, true);
1095
+ assert.equal(result.details?.usageBudget?.reason, "tokens");
1096
+ });
1097
+
1098
+ it("blocks queued foreground parallel children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1099
+ mockPi.onCall({ output: "first result" });
1100
+ const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
1101
+
1102
+ const result = await executor.execute(
1103
+ "foreground-parallel-usage-budget",
1104
+ {
1105
+ tasks: [
1106
+ { agent: "echo", task: "First task" },
1107
+ { agent: "second", task: "Second task" },
1108
+ ],
1109
+ concurrency: 1,
1110
+ usageBudget: { tokens: { hard: 10 } },
1111
+ },
1112
+ new AbortController().signal,
1113
+ undefined,
1114
+ makeMinimalCtx(tempDir),
1115
+ );
1116
+
1117
+ assert.equal(result.isError, undefined);
1118
+ assert.equal(mockPi.callCount(), 1);
1119
+ assert.equal(result.details?.results.length, 2);
1120
+ assert.equal(result.details?.results[1]?.skipped, true);
1121
+ assert.match(result.details?.results[1]?.error ?? "", /Usage budget exhausted/);
1122
+ assert.equal(result.details?.usageBudget?.exhausted, true);
1123
+ });
1124
+
1052
1125
  it("fails implementation runs that complete without mutation attempts", async () => {
1053
1126
  mockPi.onCall({ output: "Validation:\nlet rawFilename = params.filename.trim();" });
1054
1127
  const agents = [makeAgent("worker")];
@@ -1318,6 +1391,17 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1318
1391
  assert.equal(result.finalOutput, "Applied edit");
1319
1392
  });
1320
1393
 
1394
+ it("resolves explicit agent aliases to canonical execution names", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1395
+ mockPi.onCall({ output: "Implemented" });
1396
+ const executor = makeExecutor([makeAgent("worker", { aliases: ["developer"], completionGuard: false })]);
1397
+
1398
+ const result = await executor.execute("single", { agent: "developer", task: "Implement" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
1399
+
1400
+ assert.equal(result.isError, undefined);
1401
+ assert.equal(result.details?.results[0]?.agent, "worker");
1402
+ assert.match(result.content[0]?.text ?? "", /Implemented/);
1403
+ });
1404
+
1321
1405
  it("returns error for unknown agent", async () => {
1322
1406
  const agents = makeAgentConfigs(["echo"]);
1323
1407
  const result = await runSync(tempDir, agents, "nonexistent", "Do something", {});
@@ -1416,6 +1500,114 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1416
1500
  assert.ok(result.error?.includes("Something went wrong"));
1417
1501
  });
1418
1502
 
1503
+ it("retries a zero-activity startup exit on the same model", async () => {
1504
+ mockPi.onCall({ exitCode: 1 });
1505
+ mockPi.onCall({ output: "Recovered after startup race" });
1506
+ const agents = [makeAgent("worker", {
1507
+ model: "openai/gpt-5-mini",
1508
+ fallbackModels: ["anthropic/claude-sonnet-4"],
1509
+ })];
1510
+
1511
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
1512
+ runId: "startup-retry-sync",
1513
+ acceptance: false,
1514
+ });
1515
+
1516
+ assert.equal(result.exitCode, 0);
1517
+ assert.equal(result.model, "openai/gpt-5-mini");
1518
+ assert.equal(result.finalOutput, "Recovered after startup race");
1519
+ assert.deepEqual(result.attemptedModels, ["openai/gpt-5-mini"]);
1520
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, true]);
1521
+ assert.match(result.progress.recentOutput.join("\n"), /\[startup-retry\].*same model/i);
1522
+ assert.equal(result.progress.recentOutput.filter((line) => line.startsWith("[startup-retry]")).length, 1);
1523
+ assert.equal(mockPi.callCount(), 2);
1524
+ });
1525
+
1526
+ it("does not retry a non-zero exit after tool activity", async () => {
1527
+ mockPi.onCall({ jsonl: [events.toolStart("read", { path: "package.json" })], exitCode: 1 });
1528
+ mockPi.onCall({ output: "must not run" });
1529
+ const agents = [makeAgent("worker", {
1530
+ model: "openai/gpt-5-mini",
1531
+ fallbackModels: ["anthropic/claude-sonnet-4"],
1532
+ })];
1533
+
1534
+ const result = await runSync(tempDir, agents, "worker", "Read a file", {
1535
+ runId: "startup-no-retry-after-tool",
1536
+ acceptance: false,
1537
+ });
1538
+
1539
+ assert.equal(result.exitCode, 1);
1540
+ assert.equal(result.modelAttempts?.length, 1);
1541
+ assert.equal(mockPi.callCount(), 1);
1542
+ });
1543
+
1544
+ it("does not retry a signaled child exit", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
1545
+ mockPi.onCall({ signal: "SIGKILL" });
1546
+ mockPi.onCall({ output: "must not run" });
1547
+ const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
1548
+
1549
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
1550
+ runId: "startup-no-retry-after-signal",
1551
+ acceptance: false,
1552
+ });
1553
+
1554
+ assert.equal(result.exitCode, 1);
1555
+ assert.equal(result.processSignal, "SIGKILL");
1556
+ assert.equal(result.error, "Subagent process terminated by signal SIGKILL.");
1557
+ assert.equal(result.modelAttempts?.length, 1);
1558
+ assert.equal(mockPi.callCount(), 1);
1559
+ });
1560
+
1561
+ it("prefers signal termination errors over stderr tails", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
1562
+ mockPi.onCall({ stderr: "INFO benign startup line\n", signal: "SIGTERM" });
1563
+ const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
1564
+
1565
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
1566
+ runId: "signal-error-over-stderr",
1567
+ acceptance: false,
1568
+ });
1569
+
1570
+ assert.equal(result.exitCode, 1);
1571
+ assert.equal(result.processSignal, "SIGTERM");
1572
+ assert.equal(result.error, "Subagent process terminated by signal SIGTERM.");
1573
+ assert.doesNotMatch(result.error ?? "", /INFO benign startup line/);
1574
+ });
1575
+
1576
+ it("does not retry a child exit with raw stdout diagnostics", async () => {
1577
+ mockPi.onCall({ stdoutRaw: "configuration failed before protocol startup\n", exitCode: 1 });
1578
+ mockPi.onCall({ output: "must not run" });
1579
+ const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
1580
+
1581
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
1582
+ runId: "startup-no-retry-after-stdout-diagnostic",
1583
+ acceptance: false,
1584
+ });
1585
+
1586
+ assert.equal(result.exitCode, 1);
1587
+ assert.match(result.error ?? "", /configuration failed before protocol startup/);
1588
+ assert.equal(result.modelAttempts?.length, 1);
1589
+ assert.equal(mockPi.callCount(), 1);
1590
+ });
1591
+
1592
+ it("reports an actionable error after startup retries are exhausted", async () => {
1593
+ mockPi.onCall({ exitCode: 1 });
1594
+ const agents = [makeAgent("worker", {
1595
+ model: "openai/gpt-5-mini",
1596
+ fallbackModels: ["anthropic/claude-sonnet-4"],
1597
+ })];
1598
+
1599
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
1600
+ runId: "startup-retry-exhausted-sync",
1601
+ acceptance: false,
1602
+ });
1603
+
1604
+ assert.equal(result.exitCode, 1);
1605
+ assert.equal(result.modelAttempts?.length, 4);
1606
+ assert.deepEqual(result.attemptedModels, ["openai/gpt-5-mini"]);
1607
+ assert.match(result.error ?? "", /failed to start after 4 attempts.*concurrent Pi startup race/i);
1608
+ assert.equal(mockPi.callCount(), 4);
1609
+ });
1610
+
1419
1611
  it("handles long tasks via temp file (ENAMETOOLONG prevention)", async () => {
1420
1612
  mockPi.onCall({ output: "Got it" });
1421
1613
  const longTask = "Analyze ".repeat(2000); // ~16KB
@@ -1534,6 +1726,42 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1534
1726
  assert.equal(result.usage.output, 50); // from mock
1535
1727
  });
1536
1728
 
1729
+ it("advances to a fallback model after a recovered startup race and provider failure", async () => {
1730
+ mockPi.onCall({ exitCode: 1 });
1731
+ mockPi.onCall({
1732
+ jsonl: [{
1733
+ type: "message_end",
1734
+ message: {
1735
+ role: "assistant",
1736
+ content: [{ type: "text", text: "temporary provider failure" }],
1737
+ model: "openai/gpt-5-mini",
1738
+ errorMessage: "rate limit exceeded",
1739
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
1740
+ },
1741
+ }],
1742
+ exitCode: 1,
1743
+ });
1744
+ mockPi.onCall({ output: "Recovered on fallback" });
1745
+ const agents = [makeAgent("echo", {
1746
+ model: "openai/gpt-5-mini",
1747
+ fallbackModels: ["anthropic/claude-sonnet-4"],
1748
+ })];
1749
+
1750
+ const result = await runSync(tempDir, agents, "echo", "Task", {
1751
+ runId: "startup-then-fallback-sync",
1752
+ acceptance: false,
1753
+ });
1754
+
1755
+ assert.equal(result.exitCode, 0);
1756
+ assert.equal(result.model, "anthropic/claude-sonnet-4");
1757
+ assert.deepEqual(result.attemptedModels, [
1758
+ "openai/gpt-5-mini",
1759
+ "anthropic/claude-sonnet-4",
1760
+ ]);
1761
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, true]);
1762
+ assert.equal(mockPi.callCount(), 3);
1763
+ });
1764
+
1537
1765
  it("retries with fallback models on retryable provider failures", async () => {
1538
1766
  mockPi.onCall({
1539
1767
  jsonl: [{
@@ -2020,8 +2248,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2020
2248
  });
2021
2249
 
2022
2250
  it("writes artifacts when configured", async () => {
2023
- mockPi.onCall({ output: "Result text" });
2024
- const agents = makeAgentConfigs(["echo"]);
2251
+ mockPi.onCall({
2252
+ output: "Result text",
2253
+ runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 },
2254
+ });
2255
+ const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
2256
+ const agents = [makeAgent("echo", { extensions: [privateExtension] })];
2025
2257
  const artifactsDir = path.join(tempDir, "artifacts");
2026
2258
 
2027
2259
  const result = await runSync(tempDir, agents, "echo", "Task", {
@@ -2041,8 +2273,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2041
2273
  assert.match(transcript.at(-1)?.text ?? "", /^Result text/);
2042
2274
  assert.equal(result.transcriptError, undefined);
2043
2275
  assert.ok(fs.existsSync(artifactsDir), "artifacts dir should exist");
2044
- const metadata = JSON.parse(fs.readFileSync(result.artifactPaths.metadataPath, "utf-8")) as { launchContractDigest?: string };
2276
+ const metadata = JSON.parse(fs.readFileSync(result.artifactPaths.metadataPath, "utf-8")) as { launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions };
2045
2277
  assert.equal(metadata.launchContractDigest, result.launchContractDigest);
2278
+ assert.equal(result.launchResolvedExtensions?.source, "launch-resolved");
2279
+ assert.equal(result.launchResolvedExtensions?.disableAmbientExtensions, true);
2280
+ assert.deepEqual(metadata.launchResolvedExtensions, result.launchResolvedExtensions);
2281
+ assert.deepEqual(result.runtimeAcknowledgedExtensions, { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 });
2282
+ assert.deepEqual(metadata.runtimeAcknowledgedExtensions, result.runtimeAcknowledgedExtensions);
2283
+ assert.ok(!JSON.stringify(result.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
2046
2284
  });
2047
2285
 
2048
2286
  it("routes foreground artifacts to the configured session directory", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -2331,6 +2569,53 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2331
2569
  assert.equal(mockPi.callCount(), 0);
2332
2570
  });
2333
2571
 
2572
+ it("applies the foreground timeout default without overriding explicit or agent values", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2573
+ mockPi.onCall({ output: "package default" });
2574
+ mockPi.onCall({ output: "explicit timeout" });
2575
+ mockPi.onCall({ output: "max runtime alias" });
2576
+ mockPi.onCall({ output: "agent timeout" });
2577
+
2578
+ const defaultExecutor = makeExecutor();
2579
+ const defaultResult = await defaultExecutor.execute(
2580
+ "foreground-timeout-default",
2581
+ { agent: "echo", task: "Task" },
2582
+ new AbortController().signal,
2583
+ undefined,
2584
+ makeMinimalCtx(tempDir),
2585
+ );
2586
+ assert.equal(defaultResult.details?.timeoutMs, executorMod?.DEFAULT_FOREGROUND_TIMEOUT_MS);
2587
+ assert.equal(defaultResult.details?.timeoutMs, 30 * 60 * 1000);
2588
+
2589
+ const explicitResult = await defaultExecutor.execute(
2590
+ "foreground-timeout-explicit",
2591
+ { agent: "echo", task: "Task", async: false, timeoutMs: 2_000 },
2592
+ new AbortController().signal,
2593
+ undefined,
2594
+ makeMinimalCtx(tempDir),
2595
+ );
2596
+ assert.equal(explicitResult.details?.timeoutMs, 2_000);
2597
+
2598
+ const aliasResult = await defaultExecutor.execute(
2599
+ "foreground-timeout-alias",
2600
+ { agent: "echo", task: "Task", async: false, maxRuntimeMs: 3_000 },
2601
+ new AbortController().signal,
2602
+ undefined,
2603
+ makeMinimalCtx(tempDir),
2604
+ );
2605
+ assert.equal(aliasResult.details?.timeoutMs, 3_000);
2606
+
2607
+ const agentResult = await makeExecutor([
2608
+ makeAgent("echo", { defaultTimeoutMs: 4_000 }),
2609
+ ]).execute(
2610
+ "foreground-timeout-agent-default",
2611
+ { agent: "echo", task: "Task", async: false },
2612
+ new AbortController().signal,
2613
+ undefined,
2614
+ makeMinimalCtx(tempDir),
2615
+ );
2616
+ assert.equal(agentResult.details?.timeoutMs, 4_000);
2617
+ });
2618
+
2334
2619
  it("applies agent frontmatter defaults to single-agent launches", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2335
2620
  const executor = makeExecutor([
2336
2621
  makeAgent("echo", {
@@ -3006,6 +3291,464 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3006
3291
  assert.match(result.finalOutput ?? "", /Interrupted/);
3007
3292
  });
3008
3293
 
3294
+ it("supports synchronous user detach and rejects duplicate and late detach calls", async () => {
3295
+ mockPi.onCall({ steps: [
3296
+ { delay: 500, jsonl: [events.assistantMessage("completed after user detach")] },
3297
+ ] });
3298
+ let detachActive: ((reason?: string) => boolean) | undefined;
3299
+ let detachAccepted = false;
3300
+ let duplicateAccepted = true;
3301
+ let recoveredResult: RunSyncResult | undefined;
3302
+
3303
+ const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Keep working", {
3304
+ runId: "user-foreground-detach",
3305
+ acceptance: false,
3306
+ onDetachReady: (detach: (reason?: string) => boolean) => {
3307
+ detachActive = detach;
3308
+ detachAccepted = detach("user request");
3309
+ duplicateAccepted = detach("user request");
3310
+ },
3311
+ onDetachedExit: (postExit) => { recoveredResult = postExit as RunSyncResult; },
3312
+ });
3313
+
3314
+ assert.equal(detachAccepted, true);
3315
+ assert.equal(duplicateAccepted, false);
3316
+ assert.equal(recoveredResult, undefined, "foreground result should return before the child completes");
3317
+ assert.equal(result.exitCode, -2);
3318
+ assert.equal(result.detached, true);
3319
+ assert.equal(result.detachedReason, "user request");
3320
+ assert.equal(result.finalOutput, "Detached at user request before task completion.");
3321
+ assert.equal(result.processSignal, undefined);
3322
+
3323
+ for (let attempt = 0; attempt < 100 && !recoveredResult; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3324
+ assert.ok(recoveredResult);
3325
+ assert.equal(recoveredResult.exitCode, 0);
3326
+ assert.equal(recoveredResult.processSignal, undefined);
3327
+ assert.equal(recoveredResult.finalOutput, "completed after user detach");
3328
+ assert.equal(detachActive?.("user request"), false, "detach must reject calls after child exit");
3329
+ });
3330
+
3331
+ it("produces the same authoritative terminal result attached and detached", async () => {
3332
+ mockPi.onCall({ output: "authoritative answer" });
3333
+ mockPi.onCall({ steps: [{ delay: 75, jsonl: [events.assistantMessage("authoritative answer")] }] });
3334
+ const agents = makeAgentConfigs(["echo"]);
3335
+ const attached = await runSync(tempDir, agents, "echo", "Equivalent task", {
3336
+ runId: "attached-authoritative-result",
3337
+ acceptance: false,
3338
+ });
3339
+ let terminal: RunSyncResult | undefined;
3340
+ const receipt = await runSync(tempDir, agents, "echo", "Equivalent task", {
3341
+ runId: "detached-authoritative-result",
3342
+ acceptance: false,
3343
+ onDetachReady: (detach) => assert.equal(detach("user request"), true),
3344
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3345
+ });
3346
+ assert.equal(receipt.detached, true);
3347
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3348
+ assert.ok(terminal);
3349
+ assert.deepEqual(
3350
+ {
3351
+ exitCode: terminal.exitCode,
3352
+ finalOutput: terminal.finalOutput,
3353
+ usage: terminal.usage,
3354
+ progressStatus: terminal.progress.status,
3355
+ acceptanceStatus: terminal.acceptance?.status,
3356
+ },
3357
+ {
3358
+ exitCode: attached.exitCode,
3359
+ finalOutput: attached.finalOutput,
3360
+ usage: attached.usage,
3361
+ progressStatus: attached.progress.status,
3362
+ acceptanceStatus: attached.acceptance?.status,
3363
+ },
3364
+ );
3365
+ assert.equal(terminal.detached, undefined);
3366
+ assert.equal(terminal.detachedReason, "user request");
3367
+ });
3368
+
3369
+ it("isolates every nested detach receipt field from terminal completion and later sanitization", async () => {
3370
+ const receiptReport = [
3371
+ "receipt snapshot",
3372
+ "```acceptance-report",
3373
+ JSON.stringify({
3374
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "receipt evidence" }],
3375
+ changedFiles: ["src/receipt.ts"],
3376
+ testsAddedOrUpdated: ["test/receipt.test.ts"],
3377
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
3378
+ residualRisks: [],
3379
+ noStagedFiles: true,
3380
+ }),
3381
+ "```",
3382
+ ].join("\n");
3383
+ const terminalReport = [
3384
+ "terminal answer",
3385
+ "```acceptance-report",
3386
+ JSON.stringify({
3387
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "terminal isolation verified" }],
3388
+ changedFiles: ["src/receipt.ts"],
3389
+ testsAddedOrUpdated: ["test/receipt.test.ts"],
3390
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
3391
+ residualRisks: [],
3392
+ noStagedFiles: true,
3393
+ }),
3394
+ "```",
3395
+ ].join("\n");
3396
+ mockPi.onCall({ steps: [
3397
+ { jsonl: [{
3398
+ type: "message_end",
3399
+ message: {
3400
+ role: "assistant",
3401
+ content: [{ type: "text", text: receiptReport }],
3402
+ model: "mock/test-model",
3403
+ stopReason: "toolUse",
3404
+ usage: { input: 7, output: 3, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } },
3405
+ },
3406
+ }] },
3407
+ { delay: 300, jsonl: [events.assistantMessage(terminalReport)] },
3408
+ ] });
3409
+ let detach: ((reason?: string) => boolean) | undefined;
3410
+ let detached = false;
3411
+ let terminal: RunSyncResult | undefined;
3412
+ const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Keep the receipt isolated", {
3413
+ runId: "detached-deep-receipt-isolation",
3414
+ agentContract: { version: 1 },
3415
+ acceptance: {
3416
+ level: "checked",
3417
+ criteria: [{
3418
+ id: "criterion-1",
3419
+ must: "Keep detach receipt state isolated",
3420
+ evidence: ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"],
3421
+ }],
3422
+ },
3423
+ onDetachReady: (detachAttempt) => { detach = detachAttempt; },
3424
+ onUpdate: (update: { content?: Array<{ text?: string }> }) => {
3425
+ if (detached || !update.content?.[0]?.text?.includes("receipt snapshot")) return;
3426
+ detached = detach?.("user request") === true;
3427
+ },
3428
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3429
+ });
3430
+
3431
+ assert.equal(receipt.detached, true);
3432
+ const receiptMessages = receipt.messages as Array<{
3433
+ role?: string;
3434
+ model?: string;
3435
+ content: Array<{ type?: string; text?: string; callerOwned?: boolean }>;
3436
+ }>;
3437
+ const callerOwnedReceiptText = `caller-owned mutation\n${receiptReport}`;
3438
+ (receipt.agentContract as unknown as { version: number }).version = 999;
3439
+ receiptMessages[0]!.model = "caller-owned/model";
3440
+ receiptMessages[0]!.content[0]!.text = callerOwnedReceiptText;
3441
+ receiptMessages[0]!.content[0]!.callerOwned = true;
3442
+ receiptMessages[0]!.content.push({ type: "text", text: "caller-only content" });
3443
+ receiptMessages.push({ role: "assistant", model: "caller-only/model", content: [{ type: "text", text: "caller-only message" }] });
3444
+ const mutableAcceptance = receipt.acceptance as unknown as {
3445
+ status: string;
3446
+ effectiveAcceptance: { level: string; criteria: Array<{ must: string }> };
3447
+ criteria: Array<{ must: string }>;
3448
+ };
3449
+ mutableAcceptance.status = "rejected";
3450
+ mutableAcceptance.effectiveAcceptance.level = "none";
3451
+ mutableAcceptance.effectiveAcceptance.criteria[0]!.must = "caller corrupted effective criterion";
3452
+ mutableAcceptance.criteria[0]!.must = "caller corrupted ledger criterion";
3453
+ receipt.progress.status = "failed";
3454
+ (receipt.progress as unknown as { recentOutput: string[] }).recentOutput.push("caller-only progress");
3455
+ receipt.usage.turns = 999;
3456
+ receipt.usage.input = 999;
3457
+ receipt.attemptedModels = ["caller-only/model"];
3458
+ receipt.modelAttempts = [{ success: false, exitCode: 99, error: "caller-only attempt" }];
3459
+ receipt.effects = { fileMutation: { status: "missing", expected: true, attempted: false, message: "caller-only effect" } };
3460
+ receipt.execution = { status: "failed", success: false, exitCode: 99 };
3461
+ receipt.review = { status: "blockers" };
3462
+
3463
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3464
+ assert.ok(terminal);
3465
+ assert.equal(terminal.exitCode, 0);
3466
+ assert.equal(terminal.finalOutput, "terminal answer");
3467
+ assert.deepEqual(terminal.agentContract, { version: 1 });
3468
+ assert.equal(terminal.acceptance?.status, "checked");
3469
+ assert.equal(terminal.acceptance?.runtimeChecks.every((check) => check.status === "passed"), true);
3470
+ assert.equal(terminal.progress.status, "completed");
3471
+ assert.deepEqual(terminal.usage, { turns: 2, input: 107, output: 53, cacheRead: 0, cacheWrite: 0, cost: 0.002 });
3472
+ assert.deepEqual(terminal.attemptedModels, ["mock/test-model"]);
3473
+ assert.deepEqual(terminal.modelAttempts?.map((attempt) => ({ success: attempt.success, exitCode: attempt.exitCode })), [{ success: true, exitCode: 0 }]);
3474
+ assert.equal(terminal.execution?.status, "completed");
3475
+ assert.equal(terminal.execution?.success, true);
3476
+ assert.equal(terminal.review?.status, "not-requested");
3477
+ assert.deepEqual(terminal.effects, {});
3478
+ assert.doesNotMatch(JSON.stringify(terminal.messages), /acceptance-report/);
3479
+ assert.equal(receiptMessages[0]!.content[0]!.text, callerOwnedReceiptText, "terminal report sanitization must not mutate the caller receipt");
3480
+ assert.equal(receiptMessages[0]!.content[0]!.callerOwned, true);
3481
+ assert.equal(receiptMessages.length, 2);
3482
+ });
3483
+
3484
+ it("keeps the full fallback loop and authoritative aggregation alive after detach", async () => {
3485
+ mockPi.onCall({
3486
+ jsonl: [{
3487
+ type: "message_end",
3488
+ message: {
3489
+ role: "assistant",
3490
+ content: [{ type: "text", text: "temporary provider failure" }],
3491
+ model: "openai/gpt-5-mini",
3492
+ errorMessage: "rate limit exceeded",
3493
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
3494
+ },
3495
+ }],
3496
+ exitCode: 1,
3497
+ });
3498
+ mockPi.onCall({ output: "Recovered on detached fallback" });
3499
+ const agents = [makeAgent("echo", {
3500
+ model: "openai/gpt-5-mini",
3501
+ fallbackModels: ["anthropic/claude-sonnet-4"],
3502
+ })];
3503
+ let terminal: RunSyncResult | undefined;
3504
+
3505
+ const receipt = await runSync(tempDir, agents, "echo", "Task", {
3506
+ runId: "detached-fallback-loop",
3507
+ acceptance: false,
3508
+ onDetachReady: (detach) => assert.equal(detach("user request"), true),
3509
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3510
+ });
3511
+
3512
+ assert.equal(receipt.detached, true);
3513
+ for (let attempt = 0; attempt < 300 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3514
+ assert.ok(terminal);
3515
+ assert.equal(terminal.detached, undefined, "terminal status must not remain detached");
3516
+ assert.equal(terminal.detachedReason, "user request");
3517
+ assert.equal(terminal.exitCode, 0);
3518
+ assert.equal(terminal.finalOutput, "Recovered on detached fallback");
3519
+ assert.deepEqual(terminal.attemptedModels, ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"]);
3520
+ assert.deepEqual(terminal.modelAttempts?.map((attempt) => attempt.success), [false, true]);
3521
+ assert.equal(terminal.usage.turns, 2);
3522
+ assert.equal(mockPi.callCount(), 2);
3523
+ });
3524
+
3525
+ it("terminalizes a post-receipt completion pipeline throw exactly once with strict projections", async () => {
3526
+ mockPi.onCall({ steps: [{ delay: 75, jsonl: [events.assistantMessage("answer before callback failure")] }] });
3527
+ let terminal: RunSyncResult | undefined;
3528
+ let callbackCount = 0;
3529
+ const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3530
+ runId: "detached-completion-pipeline-throw",
3531
+ acceptance: { level: "checked", criteria: ["result is checked"] },
3532
+ agentContract: { version: 1 },
3533
+ onDetachReady: (detach) => {
3534
+ assert.equal(detach("user request"), true);
3535
+ },
3536
+ onUpdate: (update: { details?: { progress?: Array<{ status?: string }> } }) => {
3537
+ if (update.details?.progress?.[0]?.status === "completed") throw new Error("terminal consumer update failed");
3538
+ },
3539
+ onDetachedExit: (result) => {
3540
+ callbackCount++;
3541
+ terminal = result as RunSyncResult;
3542
+ },
3543
+ });
3544
+ assert.equal(receipt.detached, true);
3545
+ (receipt.agentContract as unknown as { version: number }).version = 999;
3546
+ receipt.messages.push({ role: "assistant", content: [{ type: "text", text: "caller-only fallback message" }] });
3547
+ const mutableAcceptance = receipt.acceptance as unknown as {
3548
+ status: string;
3549
+ effectiveAcceptance: { level: string; criteria: Array<{ must: string }> };
3550
+ };
3551
+ mutableAcceptance.status = "accepted";
3552
+ mutableAcceptance.effectiveAcceptance.level = "none";
3553
+ mutableAcceptance.effectiveAcceptance.criteria[0]!.must = "caller-only fallback criterion";
3554
+ receipt.progress.status = "completed";
3555
+ receipt.usage.turns = 999;
3556
+ receipt.usage.input = 999;
3557
+ receipt.attemptedModels = ["caller-only/fallback"];
3558
+ receipt.modelAttempts = [{ success: true, exitCode: 0 }];
3559
+ receipt.effects = { fileMutation: { status: "observed", expected: false, attempted: true, message: "caller-only fallback effect" } };
3560
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3561
+ assert.ok(terminal);
3562
+ assert.equal(callbackCount, 1);
3563
+ assert.equal(terminal.exitCode, 1);
3564
+ assert.equal(terminal.detached, undefined);
3565
+ assert.equal(terminal.detachedReason, "user request");
3566
+ assert.equal(terminal.progress?.status, "failed");
3567
+ assert.equal(terminal.acceptance?.status, "rejected");
3568
+ assert.equal(terminal.acceptance?.runtimeChecks?.[0]?.id, "completion-pipeline");
3569
+ assert.equal(terminal.execution?.status, "failed");
3570
+ assert.equal(terminal.execution?.success, false);
3571
+ assert.equal(terminal.review?.status, "not-requested");
3572
+ assert.deepEqual(terminal.agentContract, { version: 1 });
3573
+ assert.deepEqual(terminal.effects, {});
3574
+ assert.equal(terminal.usage.turns, 0);
3575
+ assert.equal(terminal.attemptedModels, undefined);
3576
+ assert.equal(terminal.modelAttempts, undefined);
3577
+ assert.doesNotMatch(JSON.stringify(terminal.messages), /caller-only fallback message/);
3578
+ assert.match(terminal.error ?? "", /Detached completion pipeline failed after receipt/);
3579
+ });
3580
+
3581
+ it("contains a synchronous onDetachReady throw and completes attached", async () => {
3582
+ mockPi.onCall({ output: "completed while attached" });
3583
+ const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3584
+ runId: "throwing-detach-ready-consumer",
3585
+ acceptance: false,
3586
+ onDetachReady: () => {
3587
+ throw new Error("bad detach consumer");
3588
+ },
3589
+ });
3590
+ assert.equal(result.exitCode, 0);
3591
+ assert.equal(result.detached, undefined);
3592
+ assert.equal(result.finalOutput, "completed while attached");
3593
+ assert.equal(result.progress.recentOutput.some((line) => /Foreground detach callback failed: bad detach consumer/.test(line)), true);
3594
+ });
3595
+
3596
+ it("reports expected artifact post-processing I/O failures without rejecting", async () => {
3597
+ mockPi.onCall({ steps: [
3598
+ { jsonl: [events.toolStart("read", { path: "README.md" })] },
3599
+ { delay: 50, jsonl: [events.assistantMessage("artifact answer")] },
3600
+ ] });
3601
+ const artifactsDir = path.join(tempDir, "artifact-output-failure");
3602
+ let sabotaged = false;
3603
+ const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3604
+ runId: "artifact-output-result-field",
3605
+ acceptance: false,
3606
+ artifactsDir,
3607
+ artifactConfig: { enabled: true },
3608
+ onUpdate: (update: { details?: { results?: Array<{ artifactPaths?: { outputPath?: string } }> } }) => {
3609
+ const outputPath = update.details?.results?.[0]?.artifactPaths?.outputPath;
3610
+ if (sabotaged || !outputPath) return;
3611
+ sabotaged = true;
3612
+ fs.mkdirSync(outputPath);
3613
+ },
3614
+ });
3615
+ assert.equal(sabotaged, true);
3616
+ assert.equal(result.exitCode, 0);
3617
+ assert.match(result.outputSaveError ?? "", /Artifact output post-processing failed/);
3618
+ });
3619
+
3620
+ it("publishes detach despite best-effort receipt metadata persistence failure", async () => {
3621
+ mockPi.onCall({ steps: [{ delay: 100, jsonl: [events.assistantMessage("completed after metadata recovery")] }] });
3622
+ const artifactsDir = path.join(tempDir, "receipt-metadata-failure");
3623
+ let terminal: RunSyncResult | undefined;
3624
+ const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3625
+ runId: "detach-receipt-metadata-failure",
3626
+ acceptance: false,
3627
+ artifactsDir,
3628
+ artifactConfig: { enabled: true },
3629
+ onDetachReady: (detach) => {
3630
+ fs.rmSync(artifactsDir, { recursive: true, force: true });
3631
+ fs.writeFileSync(artifactsDir, "block metadata", "utf-8");
3632
+ assert.equal(detach("user request"), true);
3633
+ fs.rmSync(artifactsDir, { force: true });
3634
+ fs.mkdirSync(artifactsDir, { recursive: true });
3635
+ },
3636
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3637
+ });
3638
+ assert.equal(receipt.detached, true);
3639
+ assert.ok(receipt.metadataSaveError, "receipt should record best-effort metadata persistence failure");
3640
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3641
+ assert.equal(terminal?.exitCode, 0);
3642
+ });
3643
+
3644
+ it("contains a throwing detached-exit callback", async () => {
3645
+ mockPi.onCall({ steps: [{ delay: 50, jsonl: [events.assistantMessage("done")] }] });
3646
+ let callbackCount = 0;
3647
+ const receipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3648
+ runId: "throwing-detached-exit-callback",
3649
+ acceptance: false,
3650
+ onDetachReady: (detach) => assert.equal(detach("user request"), true),
3651
+ onDetachedExit: () => {
3652
+ callbackCount++;
3653
+ throw new Error("consumer callback failed");
3654
+ },
3655
+ });
3656
+ assert.equal(receipt.detached, true);
3657
+ for (let attempt = 0; attempt < 100 && callbackCount === 0; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3658
+ assert.equal(callbackCount, 1);
3659
+ });
3660
+
3661
+ it("skips acceptance evaluation when an explicitly interrupted detached result settles", async () => {
3662
+ mockPi.onCall({ steps: [{ delay: 10_000, jsonl: [events.assistantMessage("too late")] }] });
3663
+ const interrupt = new AbortController();
3664
+ let terminal: RunSyncResult | undefined;
3665
+ const receipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Task", {
3666
+ runId: "detached-interrupted-acceptance",
3667
+ acceptance: { level: "checked", criteria: ["result is checked"] },
3668
+ interruptSignal: interrupt.signal,
3669
+ onDetachReady: (detach) => {
3670
+ assert.equal(detach("user request"), true);
3671
+ setTimeout(() => interrupt.abort(), 25);
3672
+ },
3673
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3674
+ });
3675
+ assert.equal(receipt.detached, true);
3676
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3677
+ assert.ok(terminal);
3678
+ assert.equal(terminal.interrupted, true);
3679
+ assert.equal(terminal.exitCode, 0);
3680
+ assert.equal(terminal.acceptance?.status, "pending");
3681
+ assert.equal(terminal.acceptance?.runtimeChecks[0]?.status, "not-applicable");
3682
+ assert.equal(terminal.error, undefined);
3683
+ });
3684
+
3685
+ it("linearizes originating abort against detach and keeps explicit interrupt routable afterward", async () => {
3686
+ mockPi.onCall({ steps: [{ delay: 10_000, jsonl: [events.assistantMessage("too late")] }] });
3687
+ const origin = new AbortController();
3688
+ const interrupt = new AbortController();
3689
+ let terminal: RunSyncResult | undefined;
3690
+ const receipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Keep working", {
3691
+ runId: "detach-origin-abort-race",
3692
+ acceptance: false,
3693
+ signal: origin.signal,
3694
+ interruptSignal: interrupt.signal,
3695
+ onDetachReady: (detach) => {
3696
+ assert.equal(detach("user request"), true);
3697
+ origin.abort();
3698
+ setTimeout(() => interrupt.abort(), 50);
3699
+ },
3700
+ onDetachedExit: (result) => { terminal = result as RunSyncResult; },
3701
+ });
3702
+
3703
+ assert.equal(receipt.detached, true);
3704
+ for (let attempt = 0; attempt < 100 && !terminal; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3705
+ assert.ok(terminal);
3706
+ assert.equal(terminal.interrupted, true, "explicit control interrupt must remain active after detach");
3707
+ assert.equal(terminal.processSignal, "SIGINT");
3708
+ assert.equal(terminal.detached, undefined);
3709
+ });
3710
+
3711
+ it("lets an already-observed originating abort win over detach", async () => {
3712
+ mockPi.onCall({ delay: 10_000 });
3713
+ const origin = new AbortController();
3714
+ let detachAccepted = true;
3715
+ const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Abort first", {
3716
+ runId: "origin-abort-wins-detach",
3717
+ signal: origin.signal,
3718
+ onDetachReady: (detach) => {
3719
+ origin.abort();
3720
+ detachAccepted = detach("user request");
3721
+ },
3722
+ });
3723
+ assert.equal(detachAccepted, false);
3724
+ assert.equal(result.detached, undefined);
3725
+ });
3726
+
3727
+ it("keeps the configured runtime timeout active after user detach", async () => {
3728
+ mockPi.onCall({ delay: 10_000 });
3729
+ let recoveredResult: RunSyncResult | undefined;
3730
+ const startedAt = Date.now();
3731
+
3732
+ const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Do not run forever", {
3733
+ runId: "user-detach-timeout",
3734
+ timeoutMs: 150,
3735
+ acceptance: false,
3736
+ onDetachReady: (detach: (reason?: string) => boolean) => {
3737
+ assert.equal(detach("user request"), true);
3738
+ },
3739
+ onDetachedExit: (postExit) => { recoveredResult = postExit as RunSyncResult; },
3740
+ });
3741
+
3742
+ assert.equal(result.detached, true);
3743
+ assert.ok(Date.now() - startedAt < 1_000, "detach should release the foreground waiter promptly");
3744
+ for (let attempt = 0; attempt < 300 && !recoveredResult; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
3745
+ assert.ok(recoveredResult, "configured timeout should terminate and recover the detached child");
3746
+ assert.equal(recoveredResult.timedOut, true);
3747
+ assert.equal(recoveredResult.error, "Subagent timed out after 150ms.");
3748
+ assert.equal(recoveredResult.progress.status, "failed");
3749
+ assert.ok(Date.now() - startedAt < 5_000, "detached child should remain bounded by runtime enforcement");
3750
+ });
3751
+
3009
3752
  for (const toolName of ["intercom", "contact_supervisor"]) {
3010
3753
  it(`detaches cleanly on ${toolName} handoff without aborting the child process`, async () => {
3011
3754
  const eventBus = createEventBus();
@@ -3052,6 +3795,106 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3052
3795
  });
3053
3796
  }
3054
3797
 
3798
+ it("reports intercom detach race losses and repeated requests as not accepted", async () => {
3799
+ const abortBus = createEventBus();
3800
+ const abortResponses: boolean[] = [];
3801
+ abortBus.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => abortResponses.push((payload as { accepted: boolean }).accepted));
3802
+ mockPi.onCall({ steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }, { delay: 10_000 }] });
3803
+ const origin = new AbortController();
3804
+ let requested = false;
3805
+ const abortedResult = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3806
+ runId: "intercom-abort-race-loss",
3807
+ allowIntercomDetach: true,
3808
+ intercomEvents: abortBus,
3809
+ signal: origin.signal,
3810
+ onUpdate: (update) => {
3811
+ if (requested || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
3812
+ requested = true;
3813
+ origin.abort();
3814
+ abortBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "abort-race" });
3815
+ },
3816
+ });
3817
+ assert.equal(abortedResult.detached, undefined);
3818
+ assert.deepEqual(abortResponses, [false]);
3819
+
3820
+ const repeatedBus = createEventBus();
3821
+ const repeatedResponses: boolean[] = [];
3822
+ repeatedBus.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => repeatedResponses.push((payload as { accepted: boolean }).accepted));
3823
+ mockPi.onCall({ steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }, { delay: 50, jsonl: [events.assistantMessage("done")] }] });
3824
+ let repeated = false;
3825
+ const repeatedReceipt = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
3826
+ runId: "intercom-repeated-detach",
3827
+ allowIntercomDetach: true,
3828
+ intercomEvents: repeatedBus,
3829
+ onUpdate: (update) => {
3830
+ if (repeated || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
3831
+ repeated = true;
3832
+ repeatedBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "first" });
3833
+ repeatedBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "second" });
3834
+ },
3835
+ });
3836
+ assert.equal(repeatedReceipt.detached, true);
3837
+ assert.deepEqual(repeatedResponses, [true, false]);
3838
+ });
3839
+
3840
+ it("does not launch retries or fallbacks after intercom detach and keeps timeout enforcement", async () => {
3841
+ const fallbackBus = createEventBus();
3842
+ mockPi.onCall({
3843
+ steps: [{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need decision" })] }],
3844
+ stderr: "rate limit exceeded",
3845
+ exitCode: 1,
3846
+ });
3847
+ mockPi.onCall({ output: "must not launch" });
3848
+ let resolveFallbackTerminal!: (result: RunSyncResult) => void;
3849
+ const fallbackTerminal = new Promise<RunSyncResult>((resolve) => { resolveFallbackTerminal = resolve; });
3850
+ let fallbackRequested = false;
3851
+ const receipt = await runSync(tempDir, [makeAgent("echo", { model: "openai/gpt-5-mini", fallbackModels: ["anthropic/claude-sonnet-4"] })], "echo", "Task", {
3852
+ runId: "intercom-no-fallback",
3853
+ acceptance: false,
3854
+ allowIntercomDetach: true,
3855
+ intercomEvents: fallbackBus,
3856
+ onUpdate: (update) => {
3857
+ if (fallbackRequested || !update.details?.progress?.some((item) => item.currentTool === "contact_supervisor")) return;
3858
+ fallbackRequested = true;
3859
+ fallbackBus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "no-fallback" });
3860
+ },
3861
+ onDetachedExit: (result) => { resolveFallbackTerminal(result as RunSyncResult); },
3862
+ });
3863
+ assert.equal(receipt.detached, true);
3864
+ const fallbackResult = await fallbackTerminal;
3865
+ assert.equal(mockPi.callCount(), 1);
3866
+ assert.equal(fallbackResult.exitCode, 1);
3867
+
3868
+ const timeoutBus = createEventBus();
3869
+ mockPi.reset();
3870
+ mockPi.onCall({ delay: 10_000 });
3871
+ let resolveTimeoutTerminal!: (result: RunSyncResult) => void;
3872
+ const timeoutTerminal = new Promise<RunSyncResult>((resolve) => { resolveTimeoutTerminal = resolve; });
3873
+ let timeoutRequested = false;
3874
+ const timeoutReceipt = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Task", {
3875
+ runId: "intercom-timeout-enforced",
3876
+ acceptance: false,
3877
+ timeoutMs: 125,
3878
+ allowIntercomDetach: true,
3879
+ intercomEvents: timeoutBus,
3880
+ onDetachReady: () => {
3881
+ if (timeoutRequested) return;
3882
+ timeoutRequested = true;
3883
+ timeoutBus.emit(INTERCOM_DETACH_REQUEST_EVENT, {
3884
+ requestId: "timeout",
3885
+ runId: "intercom-timeout-enforced",
3886
+ agent: "slow",
3887
+ childIndex: 0,
3888
+ });
3889
+ },
3890
+ onDetachedExit: (result) => { resolveTimeoutTerminal(result as RunSyncResult); },
3891
+ });
3892
+ assert.equal(timeoutReceipt.detached, true);
3893
+ const timeoutResult = await timeoutTerminal;
3894
+ assert.equal(timeoutResult.timedOut, true);
3895
+ assert.equal(timeoutResult.exitCode, 1);
3896
+ });
3897
+
3055
3898
  it("enforces the stdout protocol limit after foreground detachment", async () => {
3056
3899
  const eventBus = createEventBus();
3057
3900
  mockPi.onCall({ steps: [