@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
@@ -42,12 +42,12 @@ function once(events: FakeEvents, event: string): Promise<unknown> {
42
42
  });
43
43
  }
44
44
 
45
- function ctx() {
45
+ function ctx(sessionId = "session-123", sessionFile = "/sessions/parent.jsonl") {
46
46
  return {
47
47
  cwd: "/repo",
48
48
  sessionManager: {
49
- getSessionId: () => "session-123",
50
- getSessionFile: () => "/sessions/parent.jsonl",
49
+ getSessionId: () => sessionId,
50
+ getSessionFile: () => sessionFile,
51
51
  },
52
52
  } as any;
53
53
  }
@@ -91,6 +91,14 @@ describe("subagent extension RPC bridge", () => {
91
91
  (reply as { data: { capabilities?: { nonRecoveringSteer?: boolean } } }).data.capabilities?.nonRecoveringSteer,
92
92
  true,
93
93
  );
94
+ assert.equal(
95
+ (reply as { data: { capabilities?: { resume?: boolean } } }).data.capabilities?.resume,
96
+ true,
97
+ );
98
+ assert.deepEqual(
99
+ (reply as { data: { capabilities?: { fleetStatus?: unknown } } }).data.capabilities?.fleetStatus,
100
+ { version: 1 },
101
+ );
94
102
 
95
103
  bridge.dispose();
96
104
  });
@@ -137,7 +145,149 @@ describe("subagent extension RPC bridge", () => {
137
145
  assert.equal(reply.success, true);
138
146
  assert.deepEqual(executedParams, { action: "status", id: "abc123" });
139
147
  assert.equal((reply as { data: { text?: string } }).data.text, "Run: abc123");
148
+ assert.deepEqual((reply as { data: { fleet?: unknown } }).data.fleet, {
149
+ version: 1, entries: [], totalActive: 0, omitted: 0,
150
+ });
151
+
152
+ bridge.dispose();
153
+ });
154
+
155
+ it("projects bounded display-safe active fleet records without internal ids", async () => {
156
+ const events = new FakeEvents();
157
+ const state = {
158
+ currentSessionId: "/sessions/parent.jsonl",
159
+ foregroundControls: new Map(),
160
+ asyncJobs: new Map([["async-private-id", {
161
+ asyncId: "async-private-id", sessionId: "/sessions/parent.jsonl", status: "running", mode: "single",
162
+ description: ["Review", "\u001b]8;;hostile\u0007", "the diff"].join("\n"),
163
+ startedAt: 100, steps: [{ agent: "reviewer", label: "opaque label", status: "running", startedAt: 120, model: "anthropic/claude-opus-4-8:high", thinking: "high", tokens: { input: 12, output: 34, total: 46 } }],
164
+ }]]),
165
+ } as any;
166
+ const bridge = registerSubagentRpcBridge({
167
+ events, getContext: () => ctx("runtime-session-id", "/sessions/parent.jsonl"), state,
168
+ execute: async () => ({ content: [{ type: "text", text: "Active async runs: 1" }], details: { mode: "management", results: [] } } as any),
169
+ });
170
+ const reply = await request(events, "fleet-status", "status");
171
+ const fleet = (reply as { data: { fleet: { entries: Array<Record<string, unknown>> } } }).data.fleet;
172
+ assert.equal(fleet.entries.length, 1);
173
+ assert.equal((fleet as { totalActive?: number }).totalActive, 1);
174
+ assert.equal((fleet as { omitted?: number }).omitted, 0);
175
+ assert.deepEqual(fleet.entries[0], {
176
+ key: "fleet-1", agent: "reviewer", role: "opaque label", model: "anthropic/claude-opus-4-8:high", effort: "high",
177
+ startedAt: 120, tokens: { input: 12, output: 34, total: 46 }, goal: "Review the diff",
178
+ });
179
+ assert.equal(JSON.stringify(fleet).includes("async-private-id"), false);
180
+ bridge.dispose();
181
+ });
182
+
183
+ it("projects resolved foreground model, effort, split usage, and goal", async () => {
184
+ const events = new FakeEvents();
185
+ const state = {
186
+ currentSessionId: "session-123",
187
+ foregroundControls: new Map([["private-run", {
188
+ runId: "private-run",
189
+ sessionId: "session-123",
190
+ mode: "single",
191
+ startedAt: 90,
192
+ activeChildren: new Map([[0, {
193
+ index: 0,
194
+ agent: "worker",
195
+ description: "Implement the fix",
196
+ startedAt: 100,
197
+ updatedAt: 110,
198
+ model: "openai/gpt-5.6-terra:high",
199
+ thinking: "high",
200
+ inputTokens: 321,
201
+ outputTokens: 45,
202
+ tokens: 366,
203
+ }]]),
204
+ }]]),
205
+ asyncJobs: new Map(),
206
+ } as any;
207
+ state.foregroundControls.set("private-old", {
208
+ runId: "private-old",
209
+ sessionId: "old-session",
210
+ mode: "single",
211
+ startedAt: 50,
212
+ currentAgent: "reviewer",
213
+ description: "Old work",
214
+ });
215
+ const bridge = registerSubagentRpcBridge({
216
+ events,
217
+ getContext: () => ctx("session-123", "session-123"),
218
+ state,
219
+ execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any),
220
+ });
221
+ const reply = await request(events, "foreground-fleet", "status");
222
+ assert.deepEqual((reply as any).data.fleet, {
223
+ version: 1,
224
+ totalActive: 1,
225
+ omitted: 0,
226
+ entries: [{
227
+ key: "fleet-1",
228
+ agent: "worker",
229
+ model: "openai/gpt-5.6-terra:high",
230
+ effort: "high",
231
+ startedAt: 100,
232
+ tokens: { input: 321, output: 45, total: 366 },
233
+ goal: "Implement the fix",
234
+ }],
235
+ });
236
+ assert.equal(JSON.stringify((reply as any).data.fleet).includes("private-run"), false);
237
+ bridge.dispose();
238
+ });
239
+
240
+ it("uses monotonic opaque keys across removal/insertion and resets them per session", async () => {
241
+ const events = new FakeEvents();
242
+ const jobs = new Map<string, any>([
243
+ ["private-a", { asyncId: "private-a", sessionId: "A", status: "running", mode: "single", startedAt: 1, agents: ["alpha"] }],
244
+ ["private-b", { asyncId: "private-b", sessionId: "A", status: "running", mode: "single", startedAt: 2, agents: ["beta"] }],
245
+ ["private-unattributed", { asyncId: "private-unattributed", status: "running", mode: "single", startedAt: 3, agents: ["unknown"] }],
246
+ ]);
247
+ const state = { currentSessionId: "A", foregroundControls: new Map(), asyncJobs: jobs } as any;
248
+ let activeSession = "A";
249
+ const bridge = registerSubagentRpcBridge({ events, getContext: () => ctx(activeSession, activeSession), state, execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any) });
250
+ const keys = async (id: string) => ((await request(events, id, "status")) as any).data.fleet.entries.map((entry: { key: string }) => entry.key);
251
+ assert.deepEqual(await keys("keys-a"), ["fleet-1", "fleet-2"]);
252
+ jobs.delete("private-b"); jobs.set("private-c", { asyncId: "private-c", sessionId: "A", status: "running", mode: "single", startedAt: 3, agents: ["gamma"] });
253
+ assert.deepEqual(await keys("keys-b"), ["fleet-1", "fleet-3"]);
254
+ state.currentSessionId = "B";
255
+ activeSession = "B";
256
+ assert.deepEqual(await keys("keys-c"), []);
257
+ jobs.set("private-d", { asyncId: "private-d", sessionId: "B", status: "running", mode: "single", startedAt: 4, agents: ["delta"] });
258
+ assert.deepEqual(await keys("keys-d"), ["fleet-1"]);
259
+ bridge.dispose();
260
+ });
140
261
 
262
+ it("reports bounded overflow and excludes unattributed or foreign-session jobs", async () => {
263
+ const events = new FakeEvents();
264
+ const jobs = new Map<string, any>();
265
+ for (let index = 0; index < 18; index += 1) {
266
+ jobs.set(`private-${index}`, {
267
+ asyncId: `private-${index}`,
268
+ sessionId: "session-123",
269
+ status: "running",
270
+ mode: "single",
271
+ startedAt: index + 1,
272
+ agents: [`worker-${index}`],
273
+ });
274
+ }
275
+ jobs.set("unattributed", { asyncId: "unattributed", status: "running", mode: "single", startedAt: 20, agents: ["hidden"] });
276
+ jobs.set("foreign", { asyncId: "foreign", sessionId: "other", status: "running", mode: "single", startedAt: 21, agents: ["hidden"] });
277
+ const state = { currentSessionId: "session-123", foregroundControls: new Map(), asyncJobs: jobs } as any;
278
+ const bridge = registerSubagentRpcBridge({
279
+ events,
280
+ getContext: () => ctx("session-123", "session-123"),
281
+ state,
282
+ execute: async () => ({ content: [], details: { mode: "management", results: [] } } as any),
283
+ });
284
+ const reply = await request(events, "fleet-overflow", "status");
285
+ const fleet = (reply as any).data.fleet;
286
+ assert.equal(fleet.entries.length, 16);
287
+ assert.equal(fleet.totalActive, 18);
288
+ assert.equal(fleet.omitted, 2);
289
+ assert.equal(JSON.stringify(fleet).includes("unattributed"), false);
290
+ assert.equal(JSON.stringify(fleet).includes("foreign"), false);
141
291
  bridge.dispose();
142
292
  });
143
293
 
@@ -298,6 +448,75 @@ describe("subagent extension RPC bridge", () => {
298
448
  bridge.dispose();
299
449
  });
300
450
 
451
+ it("delegates resume through the existing package-owned revival action", async () => {
452
+ const events = new FakeEvents();
453
+ let executedParams: unknown;
454
+ const bridge = registerSubagentRpcBridge({
455
+ events,
456
+ getContext: () => ctx(),
457
+ execute: async (_id, params) => {
458
+ executedParams = params;
459
+ return {
460
+ content: [{ type: "text", text: "Revived async subagent from run-1." }],
461
+ details: { mode: "single", results: [], asyncId: "run-2", asyncDir: "/tmp/run-2" },
462
+ } as any;
463
+ },
464
+ });
465
+
466
+ const reply = await request(events, "resume-1", "resume", {
467
+ id: "run-1",
468
+ index: 0,
469
+ message: " Continue with the focused review. ",
470
+ output: " /tmp/revived-output.md ",
471
+ outputMode: "file-only",
472
+ });
473
+
474
+ assert.equal(reply.success, true);
475
+ assert.deepEqual(executedParams, {
476
+ action: "resume",
477
+ id: "run-1",
478
+ index: 0,
479
+ message: "Continue with the focused review.",
480
+ output: "/tmp/revived-output.md",
481
+ outputMode: "file-only",
482
+ });
483
+ assert.equal((reply as { data: { details?: { asyncId?: string } } }).data.details?.asyncId, "run-2");
484
+
485
+ bridge.dispose();
486
+ });
487
+
488
+ it("rejects targetless or empty RPC resume before executor dispatch", async () => {
489
+ const events = new FakeEvents();
490
+ let executeCalls = 0;
491
+ const bridge = registerSubagentRpcBridge({
492
+ events,
493
+ getContext: () => ctx(),
494
+ execute: async () => {
495
+ executeCalls++;
496
+ return { content: [], details: { mode: "management", results: [] } } as any;
497
+ },
498
+ });
499
+
500
+ const targetless = await request(events, "resume-no-target", "resume", { message: "continue" });
501
+ const empty = await request(events, "resume-empty", "resume", { id: "run-1", message: " " });
502
+ const inlineOutput = await request(events, "resume-inline", "resume", {
503
+ id: "run-1",
504
+ message: "continue",
505
+ output: "/tmp/output.md",
506
+ outputMode: "inline",
507
+ });
508
+
509
+ assert.equal(targetless.success, false);
510
+ assert.equal((targetless as { error: { code: string } }).error.code, "invalid_params");
511
+ assert.match((targetless as { error: { message: string } }).error.message, /requires id, runId, or dir/);
512
+ assert.equal(empty.success, false);
513
+ assert.equal((empty as { error: { code: string } }).error.code, "invalid_params");
514
+ assert.equal(inlineOutput.success, false);
515
+ assert.match((inlineOutput as { error: { message: string } }).error.message, /file-only/);
516
+ assert.equal(executeCalls, 0);
517
+ bridge.dispose();
518
+ });
519
+
301
520
  it("delegates interrupt through the existing executor action", async () => {
302
521
  const events = new FakeEvents();
303
522
  let executedParams: unknown;
@@ -328,7 +547,7 @@ describe("subagent extension RPC bridge", () => {
328
547
  fs.mkdirSync(asyncDir, { recursive: true });
329
548
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
330
549
  runId: "run-stop",
331
- sessionId: "session-123",
550
+ sessionId: "/sessions/parent.jsonl",
332
551
  mode: "single",
333
552
  state: "running",
334
553
  pid: 4242,
@@ -994,6 +994,36 @@ describe("async run status inspection", () => {
994
994
  }
995
995
  });
996
996
 
997
+ it("shows signal-terminated result-only runs as stopped", () => {
998
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-signal-result-"));
999
+ try {
1000
+ const asyncRoot = path.join(root, "runs");
1001
+ const resultsDir = path.join(root, "results");
1002
+ fs.mkdirSync(path.join(asyncRoot, "run-signal-result"), { recursive: true });
1003
+ fs.mkdirSync(resultsDir, { recursive: true });
1004
+ fs.writeFileSync(path.join(resultsDir, "run-signal-result.json"), JSON.stringify({
1005
+ id: "run-signal-result",
1006
+ agent: "worker",
1007
+ success: false,
1008
+ state: "failed",
1009
+ summary: "Subagent process terminated by signal SIGTERM.",
1010
+ results: [{ agent: "worker", success: false, exitCode: 1, processSignal: "SIGTERM" }],
1011
+ }, null, 2), "utf-8");
1012
+
1013
+ const result = inspectSubagentStatus({ id: "run-signal-result" }, {
1014
+ asyncDirRoot: asyncRoot,
1015
+ resultsDir,
1016
+ });
1017
+
1018
+ const text = textContent(result);
1019
+ assert.equal(result.isError, undefined);
1020
+ assert.match(text, /State: stopped/);
1021
+ assert.match(text, /Resume: unavailable; stopped runs are not resumable/);
1022
+ } finally {
1023
+ fs.rmSync(root, { recursive: true, force: true });
1024
+ }
1025
+ });
1026
+
997
1027
  it("falls back to an existing result when async dir has no status file", () => {
998
1028
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-result-fallback-"));
999
1029
  try {
@@ -0,0 +1,52 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES, isRuntimeAcknowledgedExtensionId, projectRuntimeAcknowledgedExtensions, readRuntimeAcknowledgedExtensions, sanitizeRuntimeAcknowledgedExtensions } from "../../src/runs/shared/runtime-acknowledged-extensions.ts";
7
+
8
+ describe("runtime acknowledged extension projection", () => {
9
+ it("validates opaque ids without path-like content", () => {
10
+ assert.equal(isRuntimeAcknowledgedExtensionId("com.example-extension:v1"), true);
11
+ assert.equal(isRuntimeAcknowledgedExtensionId(""), false);
12
+ assert.equal(isRuntimeAcknowledgedExtensionId("has space"), false);
13
+ assert.equal(isRuntimeAcknowledgedExtensionId("../secret"), false);
14
+ assert.equal(isRuntimeAcknowledgedExtensionId("path/to/ext"), false);
15
+ assert.equal(isRuntimeAcknowledgedExtensionId("path\\to\\ext"), false);
16
+ assert.equal(isRuntimeAcknowledgedExtensionId("x".repeat(129)), false);
17
+ });
18
+
19
+ it("deduplicates, caps ids, and counts omitted valid ids", () => {
20
+ const ids = ["dup", "dup", ...Array.from({ length: 35 }, (_, index) => `ext-${index}`), "bad id"];
21
+ const projected = projectRuntimeAcknowledgedExtensions(ids);
22
+
23
+ assert.equal(projected?.source, "child-runtime");
24
+ assert.equal(projected?.ids.length, 32);
25
+ assert.equal(projected?.ids[0], "dup");
26
+ assert.equal(projected?.omitted, 4);
27
+ assert.equal(projected?.ids.includes("bad id"), false);
28
+ });
29
+
30
+ it("sanitizes persisted projections and rejects invalid shapes", () => {
31
+ assert.deepEqual(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "child-runtime", ids: ["ok", "ok", "bad/path"], omitted: 2 }), {
32
+ version: 1,
33
+ source: "child-runtime",
34
+ ids: ["ok"],
35
+ omitted: 2,
36
+ });
37
+ assert.equal(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "launch-resolved", ids: ["ok"] }), undefined);
38
+ assert.equal(sanitizeRuntimeAcknowledgedExtensions({ version: 1, source: "child-runtime", ids: ["bad/path"] }), undefined);
39
+ });
40
+
41
+ it("rejects oversized capture files without reading them", (t) => {
42
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-ack-"));
43
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
44
+ const filePath = path.join(dir, "ack.json");
45
+
46
+ fs.writeFileSync(filePath, "x".repeat(MAX_RUNTIME_ACKNOWLEDGED_EXTENSION_FILE_BYTES + 1));
47
+ assert.equal(readRuntimeAcknowledgedExtensions(filePath), undefined);
48
+
49
+ fs.writeFileSync(filePath, JSON.stringify({ version: 1, source: "child-runtime", ids: ["ok"], omitted: 0 }));
50
+ assert.deepEqual(readRuntimeAcknowledgedExtensions(filePath), { version: 1, source: "child-runtime", ids: ["ok"], omitted: 0 });
51
+ });
52
+ });
@@ -38,6 +38,13 @@ interface SubagentParamsSchema {
38
38
  graceTurns?: { minimum?: number };
39
39
  };
40
40
  };
41
+ usageBudget?: {
42
+ properties?: {
43
+ tokens?: { properties?: { soft?: { exclusiveMinimum?: number }; hard?: { exclusiveMinimum?: number } } };
44
+ costUsd?: { properties?: { soft?: { exclusiveMinimum?: number }; hard?: { exclusiveMinimum?: number } } };
45
+ };
46
+ description?: string;
47
+ };
41
48
  id?: {
42
49
  type?: string;
43
50
  description?: string;
@@ -206,6 +213,19 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
206
213
  assert.equal(toolBudgetSchema?.properties?.hard?.minimum, 1);
207
214
  });
208
215
 
216
+ it("includes root-only reported usage budget", () => {
217
+ const usageBudgetSchema = SubagentParams?.properties?.usageBudget;
218
+ assert.ok(usageBudgetSchema, "usageBudget schema should exist");
219
+ assert.equal(usageBudgetSchema.properties?.tokens?.properties?.soft?.exclusiveMinimum, 0);
220
+ assert.equal(usageBudgetSchema.properties?.tokens?.properties?.hard?.exclusiveMinimum, 0);
221
+ assert.equal(usageBudgetSchema.properties?.costUsd?.properties?.soft?.exclusiveMinimum, 0);
222
+ assert.equal(usageBudgetSchema.properties?.costUsd?.properties?.hard?.exclusiveMinimum, 0);
223
+ assert.match(String(usageBudgetSchema.description ?? ""), /root-only/);
224
+ assert.match(String(usageBudgetSchema.description ?? ""), /running children are not stopped/i);
225
+ assert.equal(getPropertySchema(SubagentParams?.properties?.tasks?.items as JsonSchemaNode | undefined, ["usageBudget"]), undefined);
226
+ assert.equal(getPropertySchema(SubagentParams?.properties?.chain?.items as JsonSchemaNode | undefined, ["usageBudget"]), undefined);
227
+ });
228
+
209
229
  it("includes subagent control fields", () => {
210
230
  const idSchema = SubagentParams?.properties?.id;
211
231
  assert.ok(idSchema, "id schema should exist");
@@ -214,6 +234,12 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
214
234
  assert.match(String(idSchema.description ?? ""), /interrupt/i);
215
235
  assert.match(String(idSchema.description ?? ""), /steer/i);
216
236
  assert.match(String(idSchema.description ?? ""), /append-step/i);
237
+ assert.match(String(idSchema.description ?? ""), /approve-checkpoint/i);
238
+ assert.match(String(idSchema.description ?? ""), /reject-checkpoint/i);
239
+
240
+ const chainItemSchema = SubagentParams?.properties?.chain?.items;
241
+ assert.equal(chainItemSchema?.properties?.checkpoint?.type, "string");
242
+ assert.equal(chainItemSchema?.properties?.message?.type, "string");
217
243
 
218
244
  const runIdSchema = SubagentParams?.properties?.runId;
219
245
  assert.ok(runIdSchema, "runId schema should exist");
@@ -318,12 +344,18 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
318
344
  assert.ok(SubagentParams, "SubagentParams schema should exist");
319
345
  const schema = SubagentParams as unknown as JsonSchemaNode;
320
346
  const serialized = JSON.stringify(schema);
321
- assert.ok(serialized.length < 15_000, `expected compact schema under 15k chars, got ${serialized.length}`);
347
+ assert.ok(serialized.length < 16_000, `expected compact schema under 16k chars, got ${serialized.length}`);
322
348
  assert.equal(serialized.includes('"$ref"'), false);
323
349
  assert.equal(serialized.includes('"$defs"'), false);
324
350
  assert.equal(serialized.split("Optional acceptance policy.").length - 1, 1);
325
351
  assert.match(String((schema.properties as Record<string, JsonSchemaNode> | undefined)?.agent?.description ?? ""), /SINGLE mode/);
326
- assert.match(String((schema.properties as Record<string, JsonSchemaNode> | undefined)?.acceptance?.description ?? ""), /acceptance policy/);
352
+ const acceptanceDescription = String((schema.properties as Record<string, JsonSchemaNode> | undefined)?.acceptance?.description ?? "");
353
+ assert.match(acceptanceDescription, /acceptance policy/);
354
+ assert.match(acceptanceDescription, /Supported evidence kinds:/);
355
+ assert.match(acceptanceDescription, /commands-run/);
356
+ assert.match(acceptanceDescription, /changed-files/);
357
+ assert.match(acceptanceDescription, /manual-notes/);
358
+ assert.match(acceptanceDescription, /\{ level: "checked", evidence: \["commands-run", "changed-files"\] \}/);
327
359
 
328
360
  const nestedDescriptionPaths: string[] = [];
329
361
  const stack: Array<{ path: string; value: unknown }> = [{ path: "SubagentParams", value: schema }];
@@ -340,7 +340,7 @@ describe("skills filesystem fallback", () => {
340
340
  `;
341
341
  execFileSync(
342
342
  process.execPath,
343
- ["--experimental-transform-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script],
343
+ ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script],
344
344
  {
345
345
  cwd: projectRoot,
346
346
  env: {
@@ -60,7 +60,7 @@ function projectRequest(status: AsyncStatus, request: SteerRequest, states: Stee
60
60
  }
61
61
  }
62
62
 
63
- async function waitUntil<T>(read: () => T | undefined, timeoutMs = 1_000): Promise<T> {
63
+ async function waitUntil<T>(read: () => T | undefined, timeoutMs = 30_000): Promise<T> {
64
64
  const deadline = Date.now() + timeoutMs;
65
65
  while (true) {
66
66
  const value = read();
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import { Buffer } from "node:buffer";
3
+ import { describe, it } from "node:test";
4
+ import {
5
+ boundStreamedRecentOutput,
6
+ boundStreamedRecentTools,
7
+ boundStreamedToolCalls,
8
+ MAX_STREAMED_OUTPUT_LINE_CHARS,
9
+ MAX_STREAMED_RECENT_TOOLS,
10
+ MAX_STREAMED_TOOL_CALLS,
11
+ } from "../../src/shared/utils.ts";
12
+
13
+ describe("streamed progress snapshot bounds", () => {
14
+ it("keeps only the most recent tool-history entries and clones them", () => {
15
+ const recentTools = Array.from({ length: MAX_STREAMED_RECENT_TOOLS + 40 }, (_, i) => ({
16
+ tool: "read",
17
+ args: `file-${i}.ts`,
18
+ endMs: i,
19
+ }));
20
+ const bounded = boundStreamedRecentTools(recentTools);
21
+ assert.equal(bounded.length, MAX_STREAMED_RECENT_TOOLS);
22
+ // most-recent retained (chronological)
23
+ assert.equal(bounded.at(-1)?.endMs, recentTools.at(-1)?.endMs);
24
+ assert.equal(bounded[0]?.endMs, recentTools.at(-MAX_STREAMED_RECENT_TOOLS)?.endMs);
25
+ // cloned, not aliased
26
+ bounded[0]!.args = "mutated";
27
+ assert.notEqual(recentTools.at(-MAX_STREAMED_RECENT_TOOLS)?.args, "mutated");
28
+ });
29
+
30
+ it("truncates long recent-output lines but leaves short lines intact", () => {
31
+ const shortLine = "ok";
32
+ const longLine = "x".repeat(MAX_STREAMED_OUTPUT_LINE_CHARS + 5000);
33
+ const bounded = boundStreamedRecentOutput([shortLine, longLine]);
34
+ assert.equal(bounded[0], shortLine);
35
+ assert.ok(bounded[1]!.length < longLine.length);
36
+ assert.ok(bounded[1]!.startsWith("x".repeat(MAX_STREAMED_OUTPUT_LINE_CHARS)));
37
+ assert.match(bounded[1]!, /\[truncated\]$/);
38
+ });
39
+
40
+ it("prefers an existing toolCalls summary and caps it", () => {
41
+ const toolCalls = Array.from({ length: MAX_STREAMED_TOOL_CALLS + 20 }, (_, i) => ({
42
+ text: `read(${i})`,
43
+ expandedText: `read(file-${i}.ts)`,
44
+ }));
45
+ const bounded = boundStreamedToolCalls({ toolCalls, messages: undefined });
46
+ assert.equal(bounded?.length, MAX_STREAMED_TOOL_CALLS);
47
+ assert.equal(bounded?.at(-1)?.text, toolCalls.at(-1)?.text);
48
+ });
49
+
50
+ it("derives tool-call summaries from messages when none are present", () => {
51
+ const messages = [
52
+ { role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "a.ts" } }] },
53
+ { role: "assistant", content: [{ type: "toolCall", name: "grep", arguments: { pattern: "foo" } }] },
54
+ { role: "user", content: [{ type: "text", text: "ignored" }] },
55
+ ] as never;
56
+ const bounded = boundStreamedToolCalls({ toolCalls: undefined, messages });
57
+ assert.equal(bounded?.length, 2);
58
+ assert.match(bounded![0]!.text, /read/);
59
+ assert.match(bounded![1]!.text, /grep/);
60
+ });
61
+
62
+ it("returns undefined when there are no tool calls", () => {
63
+ assert.equal(boundStreamedToolCalls({ toolCalls: [], messages: [] as never }), undefined);
64
+ assert.equal(boundStreamedToolCalls({ toolCalls: undefined, messages: undefined }), undefined);
65
+ });
66
+
67
+ it("bounds a pathological running snapshot well under the child-stdout protocol cap", () => {
68
+ // Simulates a long, deep nested fan-out: many tool calls + long output lines.
69
+ const recentTools = Array.from({ length: 5000 }, (_, i) => ({ tool: "read", args: `services/pkg/file-${i}.ts`, endMs: i }));
70
+ const recentOutput = Array.from({ length: 50 }, () => "y".repeat(60_000));
71
+ const snapshot = {
72
+ recentTools: boundStreamedRecentTools(recentTools),
73
+ recentOutput: boundStreamedRecentOutput(recentOutput),
74
+ };
75
+ const bytes = Buffer.byteLength(JSON.stringify(snapshot), "utf8");
76
+ assert.ok(bytes < 256 * 1024, `bounded snapshot should be small, was ${bytes} bytes`);
77
+ });
78
+ });
@@ -16,6 +16,7 @@ import {
16
16
  SUBAGENT_STEER_INBOX_ENV,
17
17
  SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
18
18
  } from "../../src/runs/shared/pi-args.ts";
19
+ import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV } from "../../src/runs/shared/runtime-acknowledged-extensions.ts";
19
20
  import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "../../src/runs/shared/structured-output.ts";
20
21
  import { TOOL_BUDGET_ENV } from "../../src/runs/shared/tool-budget.ts";
21
22
  import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, formatChildToolDiagnostic, MCP_DIRECT_CHILD_TOOLS_ENV, readChildToolDiagnostic, REQUIRED_CHILD_TOOLS_ENV } from "../../src/runs/shared/tool-availability.ts";
@@ -128,6 +129,42 @@ function setSupervisorEnv(): void {
128
129
  }
129
130
 
130
131
  describe("subagent prompt runtime", () => {
132
+ it("collects runtime extension acknowledgements until terminal serialization", () => {
133
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-runtime-ack-"));
134
+ try {
135
+ const outputPath = path.join(dir, "acks.json");
136
+ process.env[RUNTIME_EXTENSION_ACK_PATH_ENV] = outputPath;
137
+ const runtimeHandlers = new Map<string, Array<(payload?: unknown) => unknown>>();
138
+ const extensionHandlers = new Map<string, Array<(payload?: unknown) => unknown>>();
139
+ const pushHandler = (target: Map<string, Array<(payload?: unknown) => unknown>>, event: string, handler: (payload?: unknown) => unknown): void => {
140
+ target.set(event, [...(target.get(event) ?? []), handler]);
141
+ };
142
+ const emitAll = (target: Map<string, Array<(payload?: unknown) => unknown>>, event: string, payload?: unknown): void => {
143
+ for (const handler of target.get(event) ?? []) handler(payload);
144
+ };
145
+
146
+ registerSubagentPromptRuntime({
147
+ events: { on(event: string, handler: (payload?: unknown) => unknown) { pushHandler(extensionHandlers, event, handler); } },
148
+ on(event: string, handler: (payload?: unknown) => unknown) { pushHandler(runtimeHandlers, event, handler); },
149
+ } as never);
150
+
151
+ emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "ext.one" });
152
+ emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "ext.one" });
153
+ emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "bad/path" });
154
+ runtimeHandlers.get("agent_end")?.[0]?.({});
155
+ emitAll(extensionHandlers, RUNTIME_EXTENSION_ACK_EVENT, { id: "late" });
156
+
157
+ assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf-8")), {
158
+ version: 1,
159
+ source: "child-runtime",
160
+ ids: ["ext.one"],
161
+ omitted: 0,
162
+ });
163
+ } finally {
164
+ fs.rmSync(dir, { recursive: true, force: true });
165
+ }
166
+ });
167
+
131
168
  it("nudges after the tool budget soft limit and blocks configured tools after hard", () => {
132
169
  const handlers = new Map<string, (payload: { toolName?: string }) => unknown>();
133
170
  const sent: string[] = [];
@@ -299,6 +336,10 @@ describe("subagent prompt runtime", () => {
299
336
  });
300
337
 
301
338
  it("registers child watchdog lifecycle handlers only when enabled by env", () => {
339
+ delete process.env[CHILD_WATCHDOG_CONFIG_ENV];
340
+ // Clear the ack capture env explicitly: when this test suite itself runs inside a
341
+ // pi-subagents child, the runner sets it and an extra agent_end handler registers.
342
+ delete process.env[RUNTIME_EXTENSION_ACK_PATH_ENV];
302
343
  const handlersWithout = new Map<string, unknown[]>();
303
344
  registerSubagentPromptRuntime({
304
345
  on(event: string, handler: unknown) {
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { Usage } from "../../src/shared/types.ts";
4
+ import {
5
+ MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS,
6
+ SUBAGENT_STARTUP_RETRY_DELAYS_MS,
7
+ formatSubagentStartupRetryExhaustedError,
8
+ formatSubagentStartupRetryNote,
9
+ isRetryableSubagentStartupFailure,
10
+ waitForSubagentStartupRetry,
11
+ type SubagentStartupFailureEvidence,
12
+ } from "../../src/runs/shared/subagent-startup-retry.ts";
13
+
14
+ function emptyUsage(): Usage {
15
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
16
+ }
17
+
18
+ function startupFailure(overrides: Partial<SubagentStartupFailureEvidence> = {}): SubagentStartupFailureEvidence {
19
+ return {
20
+ exitCode: 1,
21
+ messageCount: 0,
22
+ toolCount: 0,
23
+ usage: emptyUsage(),
24
+ durationMs: 20,
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("subagent startup retry", () => {
30
+ it("uses a bounded retry schedule", () => {
31
+ assert.deepEqual(SUBAGENT_STARTUP_RETRY_DELAYS_MS, [250, 750, 1500]);
32
+ });
33
+
34
+ it("classifies a short non-zero exit with no child activity", () => {
35
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure()), true);
36
+ });
37
+
38
+ it("rejects successful, long-running, and diagnosed exits", () => {
39
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ exitCode: 0 })), false);
40
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ durationMs: MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS + 1 })), false);
41
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ error: "authentication failed" })), false);
42
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ finalOutput: "partial response" })), false);
43
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ protocolError: { code: "protocol_output_limit" } })), false);
44
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ processSignal: "SIGKILL" })), false);
45
+ });
46
+
47
+ it("rejects any model, tool, usage, mutation, or lifecycle activity", () => {
48
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ messageCount: 1 })), false);
49
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ toolCount: 1 })), false);
50
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ usage: { ...emptyUsage(), input: 1 } })), false);
51
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ usage: { ...emptyUsage(), turns: 1 } })), false);
52
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ observedMutationAttempt: true })), false);
53
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ detached: true })), false);
54
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ interrupted: true })), false);
55
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ timedOut: true })), false);
56
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ stopped: true })), false);
57
+ assert.equal(isRetryableSubagentStartupFailure(startupFailure({ turnBudgetExceeded: true })), false);
58
+ });
59
+
60
+ it("formats retry and exhaustion diagnostics without task content", () => {
61
+ assert.equal(
62
+ formatSubagentStartupRetryNote({ model: "openai/test", attempt: 1, maxAttempts: 4, delayMs: 250 }),
63
+ "[startup-retry] openai/test exited before model or tool activity (attempt 1/4). Retrying the same model in 250ms.",
64
+ );
65
+ assert.match(
66
+ formatSubagentStartupRetryExhaustedError({ model: "openai/test", attempts: 4 }),
67
+ /failed to start after 4 attempts.*concurrent Pi startup race.*lower subagent concurrency/i,
68
+ );
69
+ });
70
+
71
+ it("cancels a pending retry delay", async () => {
72
+ const controller = new AbortController();
73
+ const wait = waitForSubagentStartupRetry(5000, [controller.signal]);
74
+ controller.abort();
75
+ assert.equal(await wait, false);
76
+ });
77
+ });