@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
@@ -15,7 +15,7 @@ import * as os from "node:os";
15
15
  import * as path from "node:path";
16
16
  import { createEventBus, createMockPi, createTempDir, events, makeAgent, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts";
17
17
  import type { MockPi } from "../support/helpers.ts";
18
- import { deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
18
+ import { deliverCheckpointDecisionRequest, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
19
19
  import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
20
20
  import { writeAtomicJson } from "../../src/shared/atomic-json.ts";
21
21
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
@@ -26,10 +26,35 @@ import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
26
26
  import { discoverAgents } from "../../src/agents/agents.ts";
27
27
  import { runSync } from "../../src/runs/foreground/execution.ts";
28
28
 
29
+ interface LaunchResolvedExtensions {
30
+ version?: number;
31
+ source?: string;
32
+ disableAmbientExtensions?: boolean;
33
+ runtime?: string[];
34
+ configured?: string[];
35
+ effective?: string[];
36
+ }
37
+
38
+ interface RuntimeAcknowledgedExtensions {
39
+ version?: number;
40
+ source?: string;
41
+ ids?: string[];
42
+ omitted?: number;
43
+ }
44
+
45
+ interface UsageBudgetState {
46
+ version?: number;
47
+ source?: string;
48
+ exhausted?: boolean;
49
+ reason?: string;
50
+ tokens?: { used?: number; hard?: number; exhausted?: boolean };
51
+ costUsd?: { used?: number; hard?: number; exhausted?: boolean };
52
+ }
53
+
29
54
  interface AsyncExecutionResult {
30
55
  content: Array<{ text?: string }>;
31
56
  isError?: boolean;
32
- details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string };
57
+ details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; usageBudget?: UsageBudgetState; checkpoint?: { name?: string; status?: string } };
33
58
  }
34
59
 
35
60
  interface AsyncResultPayload {
@@ -40,6 +65,8 @@ interface AsyncResultPayload {
40
65
  sessionId?: string;
41
66
  mode?: string;
42
67
  launchContractDigest?: string;
68
+ launchResolvedExtensions?: LaunchResolvedExtensions;
69
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
43
70
  summary?: string;
44
71
  error?: string;
45
72
  timeoutMs?: number;
@@ -51,7 +78,9 @@ interface AsyncResultPayload {
51
78
  wrapUpRequested?: boolean;
52
79
  totalTokens?: { input: number; output: number; total: number };
53
80
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
54
- results: Array<{ agent?: string; launchContractDigest?: string; output?: string; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
81
+ usageBudget?: UsageBudgetState;
82
+ checkpoint?: { name?: string; status?: string };
83
+ results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
55
84
  outputs?: Record<string, { text?: string; structured?: unknown }>;
56
85
  workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
57
86
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
@@ -68,6 +97,8 @@ interface AsyncStatusPayload {
68
97
  currentPath?: string;
69
98
  state?: string;
70
99
  launchContractDigest?: string;
100
+ launchResolvedExtensions?: LaunchResolvedExtensions;
101
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
71
102
  error?: string;
72
103
  timeoutMs?: number;
73
104
  deadlineAt?: number;
@@ -78,6 +109,8 @@ interface AsyncStatusPayload {
78
109
  wrapUpRequested?: boolean;
79
110
  totalTokens?: { total: number };
80
111
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
112
+ usageBudget?: UsageBudgetState;
113
+ checkpoint?: { name?: string; status?: string; message?: string };
81
114
  parallelGroups?: Array<{ start: number; count: number; stepIndex: number }>;
82
115
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
83
116
  capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
@@ -100,6 +133,8 @@ interface AsyncStatusPayload {
100
133
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
101
134
  agentContract?: { version: 1 };
102
135
  launchContractDigest?: string;
136
+ launchResolvedExtensions?: LaunchResolvedExtensions;
137
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
103
138
  execution?: { status?: string; success?: boolean; exitCode?: number };
104
139
  effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } };
105
140
  acceptance?: { status?: string };
@@ -292,6 +327,19 @@ async function waitForAsyncResultFile(id: string, timeoutMs = 15_000): Promise<s
292
327
  return resultPath;
293
328
  }
294
329
 
330
+ async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayload) => boolean, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
331
+ const statusPath = path.join(ASYNC_DIR, id, "status.json");
332
+ const deadline = Date.now() + timeoutMs;
333
+ while (Date.now() <= deadline) {
334
+ if (fs.existsSync(statusPath)) {
335
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
336
+ if (predicate(status)) return status;
337
+ }
338
+ await new Promise((resolve) => setTimeout(resolve, 50));
339
+ }
340
+ assert.fail(`Timed out waiting for async status: ${statusPath}`);
341
+ }
342
+
295
343
  async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
296
344
  const statusPath = path.join(ASYNC_DIR, id, "status.json");
297
345
  const deadline = Date.now() + timeoutMs;
@@ -435,6 +483,26 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
435
483
  assert.equal(payload.results[0]?.output, "你好 from fragmented async JSON");
436
484
  });
437
485
 
486
+ it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
487
+ mockPi.onCall({ jsonl: [mockAssistantMessage("", "tool_use")], stderr: "mock child failure", exitCode: 1 });
488
+ const id = `async-output-absent-${Date.now().toString(36)}`;
489
+ launchProtocolTest(id);
490
+ const payload = await readAsyncPayload(id);
491
+ assert.equal(payload.success, false);
492
+ assert.equal(payload.results[0]?.outputState, "absent");
493
+ assert.match(payload.results[0]?.error ?? "", /mock child failure/);
494
+ });
495
+
496
+ it("persists present output provenance when async failure follows partial output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
497
+ mockPi.onCall({ output: "usable partial answer", stderr: "mock post-output failure", exitCode: 1 });
498
+ const id = `async-output-present-${Date.now().toString(36)}`;
499
+ launchProtocolTest(id);
500
+ const payload = await readAsyncPayload(id);
501
+ assert.equal(payload.success, false);
502
+ assert.equal(payload.results[0]?.outputState, "present");
503
+ assert.equal(payload.results[0]?.output, "usable partial answer");
504
+ });
505
+
438
506
  it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
439
507
  const agentName = `contract-worker-${Date.now().toString(36)}`;
440
508
  const task = "Compare the resolved launch inputs.";
@@ -473,12 +541,16 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
473
541
  });
474
542
 
475
543
  it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
476
- mockPi.onCall({ output: "digest-bound async done" });
544
+ mockPi.onCall({
545
+ output: "digest-bound async done",
546
+ runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 },
547
+ });
477
548
  const id = `async-launch-digest-${Date.now().toString(36)}`;
549
+ const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
478
550
  const launch = executeAsyncSingle(id, {
479
551
  agent: "worker",
480
552
  task: "Exercise launch digest reporting",
481
- agentConfig: makeAgent("worker", { completionGuard: false }),
553
+ agentConfig: makeAgent("worker", { completionGuard: false, extensions: [privateExtension] }),
482
554
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
483
555
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
484
556
  shareEnabled: false,
@@ -493,6 +565,140 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
493
565
  assert.equal(payload.results[0]?.launchContractDigest, launch.details.launchContractDigest);
494
566
  assert.equal(status.launchContractDigest, launch.details.launchContractDigest);
495
567
  assert.equal(status.steps?.[0]?.launchContractDigest, launch.details.launchContractDigest);
568
+ assert.equal(launch.details.launchResolvedExtensions?.source, "launch-resolved");
569
+ assert.equal(launch.details.launchResolvedExtensions?.disableAmbientExtensions, true);
570
+ assert.deepEqual(payload.launchResolvedExtensions, launch.details.launchResolvedExtensions);
571
+ assert.deepEqual(payload.results[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
572
+ assert.deepEqual(status.launchResolvedExtensions, launch.details.launchResolvedExtensions);
573
+ assert.deepEqual(status.steps?.[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
574
+ const runtimeAck = { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 };
575
+ assert.deepEqual(payload.runtimeAcknowledgedExtensions, runtimeAck);
576
+ assert.deepEqual(payload.results[0]?.runtimeAcknowledgedExtensions, runtimeAck);
577
+ assert.deepEqual(status.runtimeAcknowledgedExtensions, runtimeAck);
578
+ assert.deepEqual(status.steps?.[0]?.runtimeAcknowledgedExtensions, runtimeAck);
579
+ assert.ok(!JSON.stringify(launch.details.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
580
+ });
581
+
582
+ it("async chain checkpoints pause before the next child until approved", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
583
+ mockPi.onCall({ output: "analysis done" });
584
+ mockPi.onCall({ output: "implementation done" });
585
+ const id = `async-checkpoint-approve-${Date.now().toString(36)}`;
586
+ const launch = executeAsyncChain(id, {
587
+ chain: [
588
+ { agent: "analyst", task: "Analyze" },
589
+ { checkpoint: "review", message: "Approve implementation?" },
590
+ { agent: "reporter", task: "Summarize {previous}" },
591
+ ],
592
+ agents: [makeAgent("analyst"), makeAgent("reporter")],
593
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
594
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
595
+ shareEnabled: false,
596
+ maxSubagentDepth: 2,
597
+ });
598
+
599
+ const paused = await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
600
+ assert.equal(launch.isError, undefined);
601
+ assert.equal(paused.steps?.[1]?.status, "paused");
602
+ assert.equal(mockPi.callCount(), 1);
603
+
604
+ deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "approved" });
605
+ const payload = await readAsyncPayload(id);
606
+ assert.equal(payload.success, true);
607
+ assert.equal(payload.state, "complete");
608
+ assert.equal(payload.checkpoint?.status, "approved");
609
+ assert.equal(payload.results.length, 2);
610
+ assert.equal(mockPi.callCount(), 2);
611
+ });
612
+
613
+ it("foreground checkpoint decisions use current-session control actions", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
614
+ mockPi.onCall({ output: "analysis done" });
615
+ const executor = makeAsyncExecutor([makeAgent("analyst"), makeAgent("worker")]);
616
+ const ctx = makeMinimalCtx(tempDir);
617
+ ctx.sessionManager.getSessionId = () => "session-foreground-checkpoint";
618
+ const id = `foreground-checkpoint-${Date.now().toString(36)}`;
619
+
620
+ const paused = await executor.execute(id, {
621
+ chain: [
622
+ { agent: "analyst", task: "Analyze" },
623
+ { checkpoint: "review", message: "Approve implementation?" },
624
+ { agent: "worker", task: "Implement" },
625
+ ],
626
+ }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
627
+
628
+ assert.equal(paused.isError, undefined);
629
+ assert.equal(paused.details.checkpoint?.status, "pending");
630
+ assert.equal(mockPi.callCount(), 1);
631
+ const runId = (paused.details as AsyncExecutionResult["details"] & { runId?: string }).runId;
632
+ assert.ok(runId);
633
+
634
+ const approved = await executor.execute(`${id}-approve`, { action: "approve-checkpoint", id: runId }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
635
+ assert.equal(approved.isError, undefined);
636
+ assert.equal(approved.details.checkpoint?.name, "review");
637
+ assert.equal(approved.details.checkpoint?.status, "approved");
638
+ assert.equal(mockPi.callCount(), 1);
639
+ });
640
+
641
+ it("async chain checkpoint rejection is an explicit terminal state", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
642
+ mockPi.onCall({ output: "analysis done" });
643
+ mockPi.onCall({ output: "should not run" });
644
+ const id = `async-checkpoint-reject-${Date.now().toString(36)}`;
645
+ executeAsyncChain(id, {
646
+ chain: [
647
+ { agent: "analyst", task: "Analyze" },
648
+ { checkpoint: "review" },
649
+ { agent: "worker", task: "Implement" },
650
+ ],
651
+ agents: [makeAgent("analyst"), makeAgent("worker")],
652
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
653
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
654
+ shareEnabled: false,
655
+ maxSubagentDepth: 2,
656
+ });
657
+
658
+ await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
659
+ deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "rejected" });
660
+ const payload = await readAsyncPayload(id);
661
+ assert.equal(payload.success, false);
662
+ assert.equal(payload.state, "rejected");
663
+ assert.equal(payload.checkpoint?.status, "rejected");
664
+ assert.equal(mockPi.callCount(), 1);
665
+ });
666
+
667
+ it("background parallel groups report usage budget state and block queued children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
668
+ mockPi.onCall({ output: "first async result" });
669
+ const id = `async-usage-budget-${Date.now().toString(36)}`;
670
+ const launch = executeAsyncChain(id, {
671
+ chain: [{
672
+ parallel: [
673
+ { agent: "first", task: "First task" },
674
+ { agent: "second", task: "Second task" },
675
+ ],
676
+ concurrency: 1,
677
+ }],
678
+ resultMode: "parallel",
679
+ usageBudget: { tokens: { hard: 10 } },
680
+ agents: [makeAgent("first"), makeAgent("second")],
681
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
682
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
683
+ shareEnabled: false,
684
+ maxSubagentDepth: 2,
685
+ });
686
+
687
+ assert.equal(launch.details.usageBudget?.exhausted, false);
688
+ const payload = await readAsyncPayload(id);
689
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
690
+ assert.equal(payload.success, false);
691
+ assert.equal(payload.state, "failed");
692
+ assert.match(payload.error ?? payload.summary ?? "", /Usage budget exhausted/);
693
+ assert.equal(payload.results.length, 2);
694
+ assert.equal(payload.results[1]?.skipped, true);
695
+ assert.match(payload.results[1]?.error ?? "", /Usage budget exhausted/);
696
+ assert.equal(mockPi.callCount(), 1);
697
+ assert.equal(payload.usageBudget?.exhausted, true);
698
+ assert.equal(payload.usageBudget?.reason, "tokens");
699
+ assert.equal(status.usageBudget?.exhausted, true);
700
+ assert.equal(status.steps?.[0]?.status, "complete");
701
+ assert.equal(status.steps?.[1]?.status, "failed");
496
702
  });
497
703
 
498
704
  it("background fails with protocol_output_limit for an oversized stdout line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -1349,13 +1555,13 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1349
1555
  tempArtifactsDir: tempDir,
1350
1556
  getSubagentSessionRoot: () => tempDir,
1351
1557
  expandTilde: (p: string) => p,
1352
- discoverAgents: () => ({ agents: [makeAgent("worker", { defaultProgress: true })] }),
1558
+ discoverAgents: () => ({ agents: [makeAgent("builder", { defaultProgress: true })] }),
1353
1559
  });
1354
1560
 
1355
1561
  const result = await executor.execute(
1356
1562
  "async-parallel-fields",
1357
1563
  {
1358
- tasks: [{ agent: "worker", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
1564
+ tasks: [{ agent: "builder", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
1359
1565
  async: true,
1360
1566
  clarify: false,
1361
1567
  },
@@ -1862,6 +2068,39 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1862
2068
  assert.equal(payload.workflowGraph?.nodes?.[2]?.flatIndex, 3);
1863
2069
  });
1864
2070
 
2071
+ it("async dynamic fanout blocks queued children when hard reported usage is exhausted", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2072
+ mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
2073
+ mockPi.onCall({ matchArgIncludes: "Review src/a.ts", output: "review-a", structuredOutput: { ok: "a" } });
2074
+ const id = `async-dynamic-usage-budget-${Date.now().toString(36)}`;
2075
+ executeAsyncChain(id, {
2076
+ chain: [
2077
+ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } },
2078
+ {
2079
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
2080
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
2081
+ collect: { as: "reviews" },
2082
+ concurrency: 1,
2083
+ },
2084
+ ],
2085
+ usageBudget: { tokens: { hard: 200 } },
2086
+ agents: [makeAgent("producer"), makeAgent("reviewer")],
2087
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-budget" },
2088
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
2089
+ shareEnabled: false,
2090
+ maxSubagentDepth: 2,
2091
+ });
2092
+
2093
+ const payload = await readAsyncPayload(id);
2094
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
2095
+ assert.equal(mockPi.callCount(), 2);
2096
+ assert.equal(payload.success, false);
2097
+ assert.equal(payload.usageBudget?.exhausted, true);
2098
+ assert.equal(status.steps?.[1]?.status, "complete");
2099
+ assert.equal(status.steps?.[2]?.status, "failed");
2100
+ assert.match(status.steps?.[2]?.error ?? "", /Usage budget exhausted/);
2101
+ assert.equal(payload.results.find((result) => result.agent === "reviewer" && result.skipped)?.skipped, true);
2102
+ });
2103
+
1865
2104
  it("rejects a shared explicit output before dynamic fanout children start", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1866
2105
  mockPi.onCall({ matchArgIncludes: "Produce targets", output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } });
1867
2106
  const id = `async-dynamic-explicit-output-${Date.now().toString(36)}`;
@@ -2360,6 +2599,43 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2360
2599
  assert.equal(mockPi.callCount(), 2);
2361
2600
  });
2362
2601
 
2602
+ it("background runs retry a zero-activity startup exit on the same model", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2603
+ mockPi.onCall({ exitCode: 1 });
2604
+ mockPi.onCall({ output: "Recovered asynchronously after startup race" });
2605
+ const id = `async-startup-retry-${Date.now().toString(36)}`;
2606
+ executeAsyncSingle(id, {
2607
+ agent: "worker",
2608
+ task: "Do work",
2609
+ agentConfig: makeAgent("worker", {
2610
+ model: "openai/gpt-5-mini:high",
2611
+ fallbackModels: ["anthropic/claude-sonnet-4:low"],
2612
+ }),
2613
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
2614
+ availableModels: [
2615
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
2616
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
2617
+ ],
2618
+ artifactConfig: {
2619
+ enabled: false,
2620
+ includeInput: false,
2621
+ includeOutput: false,
2622
+ includeJsonl: false,
2623
+ includeMetadata: false,
2624
+ cleanupDays: 7,
2625
+ },
2626
+ shareEnabled: false,
2627
+ maxSubagentDepth: 2,
2628
+ });
2629
+
2630
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")) as AsyncResultPayload;
2631
+ assert.equal(payload.success, true);
2632
+ assert.equal(payload.results[0]?.model, "openai/gpt-5-mini:high");
2633
+ assert.deepEqual(payload.results[0]?.attemptedModels, ["openai/gpt-5-mini:high"]);
2634
+ assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]);
2635
+ assert.match(payload.results[0]?.output ?? "", /\[startup-retry\].*Recovered asynchronously after startup race/s);
2636
+ assert.equal(mockPi.callCount(), 2);
2637
+ });
2638
+
2363
2639
  it("background runs retry the fallback model when the provider stream ends without finish_reason", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2364
2640
  mockPi.onCall({
2365
2641
  jsonl: [{
@@ -3143,6 +3419,71 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3143
3419
  assert.deepEqual(payload.results[0].attemptedModels, ["github-copilot/gpt-5-mini"]);
3144
3420
  });
3145
3421
 
3422
+ it("async executor keeps the last parent session model after continuation drops ctx.model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
3423
+ mockPi.onCall({ output: "Done asynchronously" });
3424
+ const state = {
3425
+ baseCwd: tempDir,
3426
+ currentSessionId: null,
3427
+ asyncJobs: new Map(),
3428
+ foregroundControls: new Map(),
3429
+ lastForegroundControlId: null,
3430
+ };
3431
+ const executor = createSubagentExecutor!({
3432
+ pi: { events: createEventBus(), getSessionName: () => undefined },
3433
+ state,
3434
+ config: {},
3435
+ asyncByDefault: false,
3436
+ tempArtifactsDir: tempDir,
3437
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
3438
+ expandTilde: (p: string) => p,
3439
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
3440
+ });
3441
+ const initialCtx = makeMinimalCtx(tempDir);
3442
+ initialCtx.sessionManager.getSessionId = () => "session-continued";
3443
+ initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
3444
+ await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
3445
+
3446
+ const continuedCtx = makeMinimalCtx(tempDir);
3447
+ continuedCtx.sessionManager.getSessionId = () => "session-continued";
3448
+ const launch = await executor.execute(
3449
+ "continued-async-child",
3450
+ { agent: "worker", task: "Do work", async: true, acceptance: false },
3451
+ new AbortController().signal,
3452
+ undefined,
3453
+ continuedCtx,
3454
+ ) as AsyncExecutionResult;
3455
+ assert.equal(launch.isError, undefined);
3456
+ assert.ok(launch.details.asyncId);
3457
+
3458
+ const payload = await readAsyncPayload(launch.details.asyncId);
3459
+ assert.equal(payload.success, true);
3460
+ assert.equal(payload.results[0]?.model, "deepseek/deepseek-v4-flash");
3461
+ const args = readMockPiArgs(mockPi, 0);
3462
+ assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3463
+ });
3464
+
3465
+ it("foreground chains keep the last parent session model after continuation drops ctx.model", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
3466
+ mockPi.onCall({ output: "Done foreground" });
3467
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
3468
+ const initialCtx = makeMinimalCtx(tempDir);
3469
+ initialCtx.sessionManager.getSessionId = () => "session-continued";
3470
+ initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
3471
+ await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
3472
+
3473
+ const continuedCtx = makeMinimalCtx(tempDir);
3474
+ continuedCtx.sessionManager.getSessionId = () => "session-continued";
3475
+ const result = await executor.execute(
3476
+ "continued-foreground-chain",
3477
+ { chain: [{ agent: "worker", task: "Do work" }], acceptance: false },
3478
+ new AbortController().signal,
3479
+ undefined,
3480
+ continuedCtx,
3481
+ );
3482
+ assert.equal(result.isError, undefined);
3483
+ const args = readMockPiArgs(mockPi, 0);
3484
+ assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3485
+ });
3486
+
3146
3487
  it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3147
3488
  mockPi.onCall({ output: "Done asynchronously" });
3148
3489
 
@@ -114,6 +114,27 @@ function createUiContext() {
114
114
  }
115
115
 
116
116
  describe("async job tracker", { skip: !available ? "pi packages not available" : undefined }, () => {
117
+ it("drops stale captured extension contexts instead of crashing background timers", () => {
118
+ const asyncRoot = createTempDir("pi-async-job-tracker-stale-context-");
119
+ try {
120
+ const state = createState();
121
+ const recorder = createEventRecorder();
122
+ const tracker = trackerMod!.createAsyncJobTracker(recorder.pi, state as never, asyncRoot);
123
+ (state as { lastUiContext: unknown }).lastUiContext = {
124
+ get hasUI() {
125
+ throw new Error("This extension ctx is stale after session replacement or reload.");
126
+ },
127
+ };
128
+
129
+ tracker.handleStarted({ id: "run-stale-context", asyncDir: path.join(asyncRoot, "run-stale-context"), agent: "worker" });
130
+
131
+ assert.equal(state.lastUiContext, null);
132
+ if (state.poller) clearInterval(state.poller);
133
+ } finally {
134
+ removeTempDir(asyncRoot);
135
+ }
136
+ });
137
+
117
138
  it("removes completed jobs after retention and requests a rerender", async () => {
118
139
  const asyncRoot = createTempDir("pi-async-job-tracker-");
119
140
  try {
@@ -856,6 +877,69 @@ describe("async job tracker", { skip: !available ? "pi packages not available" :
856
877
  }
857
878
  });
858
879
 
880
+ it("resynchronizes when a saved cursor resumes inside an oversized JSONL record", async () => {
881
+ const asyncRoot = createTempDir("pi-async-job-tracker-");
882
+ const originalError = console.error;
883
+ const errors: unknown[][] = [];
884
+ console.error = (...args: unknown[]) => {
885
+ errors.push(args);
886
+ };
887
+ try {
888
+ const runDir = path.join(asyncRoot, "run-midline-oversized-control");
889
+ fs.mkdirSync(runDir, { recursive: true });
890
+ fs.writeFileSync(path.join(runDir, "status.json"), JSON.stringify({
891
+ runId: "run-midline-oversized-control",
892
+ mode: "single",
893
+ state: "running",
894
+ startedAt: Date.now() - 1000,
895
+ lastUpdate: Date.now(),
896
+ steps: [{ agent: "worker", status: "running" }],
897
+ }), "utf-8");
898
+ const largeDiagnostic = JSON.stringify({
899
+ type: "message_update",
900
+ message: { role: "assistant", content: [{ type: "text", text: "x".repeat(2_500_000) }] },
901
+ });
902
+ const controlEvent = JSON.stringify({
903
+ type: "subagent.control",
904
+ channels: ["event"],
905
+ event: {
906
+ type: "needs_attention",
907
+ to: "needs_attention",
908
+ ts: 123,
909
+ runId: "run-midline-oversized-control",
910
+ agent: "worker",
911
+ message: "worker needs attention",
912
+ },
913
+ });
914
+ fs.writeFileSync(path.join(runDir, "events.jsonl"), `${largeDiagnostic}\n${controlEvent}\n`, "utf-8");
915
+
916
+ const state = createState();
917
+ state.asyncJobs.set("run-midline-oversized-control", {
918
+ asyncId: "run-midline-oversized-control",
919
+ asyncDir: runDir,
920
+ status: "running",
921
+ agents: ["worker"],
922
+ startedAt: Date.now() - 1000,
923
+ updatedAt: Date.now(),
924
+ controlEventCursor: 0,
925
+ });
926
+ const recorder = createEventRecorder();
927
+ const tracker = trackerMod!.createAsyncJobTracker(recorder.pi, state as never, asyncRoot, {
928
+ pollIntervalMs: 10,
929
+ });
930
+ tracker.ensurePoller();
931
+
932
+ await waitForCondition(
933
+ () => recorder.events.some((event) => event.channel === "subagent:control-event"),
934
+ "control event after oversized diagnostic",
935
+ );
936
+ assert.equal(errors.some((args) => String(args[0]).includes("Ignoring malformed async control event")), false);
937
+ } finally {
938
+ console.error = originalError;
939
+ removeTempDir(asyncRoot);
940
+ }
941
+ });
942
+
859
943
  it("does not tail-skip control events for newly tracked large logs", async () => {
860
944
  const asyncRoot = createTempDir("pi-async-job-tracker-");
861
945
  try {
@@ -27,8 +27,8 @@ describe("async status helpers", () => {
27
27
  currentStep: 1,
28
28
  outputFile,
29
29
  steps: [
30
- { agent: "scout", status: "complete", durationMs: 10 },
31
- { agent: "worker", status: "running", durationMs: 20 },
30
+ { agent: "scout", status: "complete", durationMs: 10, description: "Inspect auth only" },
31
+ { agent: "worker", status: "running", durationMs: 20, description: "Patch billing only" },
32
32
  ],
33
33
  });
34
34
  createAsyncDir(root, "run-b", {
@@ -47,6 +47,8 @@ describe("async status helpers", () => {
47
47
  assert.equal(runs[0]?.steps.length, 2);
48
48
  assert.equal(runs[0]?.steps[1]?.agent, "worker");
49
49
  assert.equal(runs[0]?.steps[1]?.status, "running");
50
+ assert.equal(runs[0]?.steps[0]?.description, "Inspect auth only");
51
+ assert.equal(runs[0]?.steps[1]?.description, "Patch billing only");
50
52
  assert.match(formatAsyncRunList(runs), /output: .*output-1\.log/);
51
53
  } finally {
52
54
  fs.rmSync(root, { recursive: true, force: true });
@@ -349,9 +349,9 @@ describe("chain clarify model display", { skip: !available ? "pi packages not av
349
349
  assert.match(initial, /\[Enter\] Run • \[Esc\] Cancel/);
350
350
  if (mode === "single") {
351
351
  assert.match(initial, /\[w\]Output/);
352
- assert.doesNotMatch(initial, /\[↑↓\] Navigate/);
352
+ assert.doesNotMatch(initial, /\[↑↓\/jk\] Navigate/);
353
353
  } else {
354
- assert.match(initial, /\[↑↓\] Navigate/);
354
+ assert.match(initial, /\[↑↓\/jk\] Navigate/);
355
355
  }
356
356
  if (mode === "chain") {
357
357
  assert.match(initial, /\[w\]Output/);
@@ -368,4 +368,53 @@ describe("chain clarify model display", { skip: !available ? "pi packages not av
368
368
  assert.match(component.render(84).map(stripAnsi).join("\n"), /\[b\]Background:ON/);
369
369
  }
370
370
  });
371
+
372
+ it("navigates non-editing chain steps with j and k", () => {
373
+ const agents = ["scout", "worker"].map((name) => ({
374
+ name,
375
+ description: "",
376
+ systemPrompt: "",
377
+ systemPromptMode: "replace",
378
+ inheritProjectContext: false,
379
+ inheritSkills: false,
380
+ source: "user",
381
+ filePath: `${name}.md`,
382
+ }));
383
+ const component = new ChainClarifyComponent(
384
+ { requestRender() {} },
385
+ { fg(_key: string, text: string) { return text; } },
386
+ agents,
387
+ ["Scout", "Implement"],
388
+ "Task",
389
+ undefined,
390
+ agents.map(() => ({ output: false, outputMode: "inline", reads: false, progress: false, skills: [], model: undefined })),
391
+ [],
392
+ undefined,
393
+ [{ name: "jk-skill", source: "user" }],
394
+ () => {},
395
+ "chain",
396
+ );
397
+
398
+ component.handleInput("j");
399
+ assert.equal(component.selectedStep, 1);
400
+ component.handleInput("k");
401
+ assert.equal(component.selectedStep, 0);
402
+
403
+ component.handleInput("e");
404
+ component.handleInput("j");
405
+ component.handleInput("k");
406
+ assert.match(component.render(84).map(stripAnsi).join("\n"), /jkScout/);
407
+ component.handleInput("\x1b");
408
+
409
+ component.handleInput("m");
410
+ component.handleInput("j");
411
+ component.handleInput("k");
412
+ assert.match(component.render(84).map(stripAnsi).join("\n"), /Search: jk/);
413
+ component.handleInput("\x1b");
414
+
415
+ component.handleInput("s");
416
+ component.handleInput("j");
417
+ component.handleInput("k");
418
+ assert.match(component.render(84).map(stripAnsi).join("\n"), /Search: jk/);
419
+ });
371
420
  });