@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.0

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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -166,6 +166,7 @@ import type {
166
166
  TurnEndEvent,
167
167
  TurnStartEvent,
168
168
  } from "../extensibility/extensions";
169
+ import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
169
170
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
170
171
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
171
172
  import type { HookCommandContext } from "../extensibility/hooks/types";
@@ -187,6 +188,7 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
187
188
  import { createPlanReadMatcher } from "../plan-mode/plan-protection";
188
189
  import type { PlanModeState } from "../plan-mode/state";
189
190
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
191
+ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
190
192
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
191
193
  import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
192
194
  import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
@@ -238,7 +240,7 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
238
240
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
239
241
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
240
242
  import { normalizeModelContextImages } from "../utils/image-loading";
241
- import { buildNamedToolChoice } from "../utils/tool-choice";
243
+ import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
242
244
  import type { AuthStorage } from "./auth-storage";
243
245
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
244
246
  import {
@@ -253,20 +255,17 @@ import {
253
255
  type CustomMessage,
254
256
  convertToLlm,
255
257
  type PythonExecutionMessage,
256
- readPendingDisplayTag,
258
+ readQueueChipText,
257
259
  SILENT_ABORT_MARKER,
258
260
  SKILL_PROMPT_MESSAGE_TYPE,
259
261
  stripImagesFromMessage,
260
262
  } from "./messages";
263
+ import type { SessionContext } from "./session-context";
264
+ import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
261
265
  import { formatSessionDumpText } from "./session-dump-format";
262
- import type {
263
- BranchSummaryEntry,
264
- CompactionEntry,
265
- NewSessionOptions,
266
- SessionContext,
267
- SessionManager,
268
- } from "./session-manager";
269
- import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
266
+ import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
267
+ import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
268
+ import type { SessionManager } from "./session-manager";
270
269
  import type { ShakeMode, ShakeResult } from "./shake-types";
271
270
  import { ToolChoiceQueue } from "./tool-choice-queue";
272
271
  import { YieldQueue } from "./yield-queue";
@@ -441,6 +440,10 @@ export interface AgentSessionConfig {
441
440
  asyncJobManager?: AsyncJobManager;
442
441
  /** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
443
442
  agentId?: string;
443
+ /** Whether this session is the top-level agent or a subagent. Drives eager-task
444
+ * prelude gating so a top-level session created with a custom `agentId` still
445
+ * receives the always-mode reminder. Defaults to "main". */
446
+ agentKind?: "main" | "sub";
444
447
  /**
445
448
  * Override the provider-facing session ID for all API requests from this session.
446
449
  * When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
@@ -862,18 +865,42 @@ function extractPermissionLocations(
862
865
  // AgentSession Class
863
866
  // ============================================================================
864
867
 
865
- /** Internal record stored in the steering/followUp display queues. The optional
866
- * `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
867
- * custom messages queued during streaming) and is matched by the custom-role
868
- * `message_start` dequeue branch; user-message pushes leave it undefined and
869
- * rely on the existing text-equality match. `images` carries the original
870
- * (pre-normalization) image blocks so queue restoration (Esc / Alt+Up) can
871
- * hand them back to the editor instead of dropping them. */
872
- type QueuedDisplayEntry = { text: string; tag?: string; images?: ImageContent[] };
873
-
874
868
  /** Entry returned by {@link AgentSession.clearQueue} / {@link AgentSession.popLastQueuedMessage}. */
875
869
  export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
876
870
 
871
+ function queuedTextContent(message: AgentMessage): string | undefined {
872
+ if (!("content" in message)) return undefined;
873
+ const content = message.content;
874
+ if (typeof content === "string") return content;
875
+ return content.find((part): part is TextContent => part.type === "text")?.text;
876
+ }
877
+
878
+ function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
879
+ if (!("content" in message) || typeof message.content === "string") return undefined;
880
+ const images = message.content.filter(
881
+ (part): part is ImageContent =>
882
+ part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string",
883
+ );
884
+ return images.length > 0 ? images : undefined;
885
+ }
886
+
887
+ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
888
+ return !(message.role === "custom" && message.display === false);
889
+ }
890
+
891
+ function queueChipText(message: AgentMessage): string {
892
+ if (message.role === "custom") {
893
+ return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
894
+ }
895
+ const text = queuedTextContent(message) ?? "";
896
+ if (text) return text;
897
+ return queuedImageContent(message) ? "[Image]" : "";
898
+ }
899
+
900
+ function toRestoredQueuedMessage(message: AgentMessage): RestoredQueuedMessage {
901
+ return { text: queueChipText(message), images: queuedImageContent(message) };
902
+ }
903
+
877
904
  export class AgentSession {
878
905
  readonly agent: Agent;
879
906
  readonly sessionManager: SessionManager;
@@ -904,18 +931,10 @@ export class AgentSession {
904
931
  #eventListeners: AgentSessionEventListener[] = [];
905
932
  #commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
906
933
 
907
- /** Tracks pending steering messages for UI display. Removed when delivered.
908
- * Entry shape: `{ text }` for plain-text steers (user-message dequeue
909
- * matches by `.text`); `{ text, tag }` for queued custom messages (skill
910
- * invocations dispatched while streaming) — the custom-role dequeue
911
- * matches by `.tag` so duplicate-args queued skills cannot collide. */
912
- #steeringMessages: QueuedDisplayEntry[] = [];
913
- /** Tracks pending follow-up messages for UI display. Removed when delivered.
914
- * See `#steeringMessages` for entry shape. */
915
- #followUpMessages: QueuedDisplayEntry[] = [];
916
934
  /** Messages queued to be included with the next user prompt as context ("asides"). */
917
935
  #pendingNextTurnMessages: CustomMessage[] = [];
918
936
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
937
+ #queuedMessageDrainScheduled = false;
919
938
  #planModeState: PlanModeState | undefined;
920
939
  #goalModeState: GoalModeState | undefined;
921
940
  #goalRuntime: GoalRuntime;
@@ -979,6 +998,7 @@ export class AgentSession {
979
998
  #pendingIrcAsides: CustomMessage[] = [];
980
999
  // Agent identity (registry id) used for IRC routing and job ownership.
981
1000
  #agentId: string | undefined;
1001
+ #agentKind: "main" | "sub" = "main";
982
1002
  #providerSessionId: string | undefined;
983
1003
  #freshProviderSessionId: string | undefined;
984
1004
  #isDisposed = false;
@@ -1058,10 +1078,6 @@ export class AgentSession {
1058
1078
  * without producing an aborted message_end). */
1059
1079
  #planCompactAbortPending = false;
1060
1080
 
1061
- /** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
1062
- * combined with `Date.now()` so tags stay unique even across rapid
1063
- * same-tick enqueues. */
1064
- #customDisplayTagCounter = 0;
1065
1081
  #postPromptTasks = new Set<Promise<unknown>>();
1066
1082
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
1067
1083
  #postPromptTasksResolve: (() => void) | undefined = undefined;
@@ -1074,6 +1090,7 @@ export class AgentSession {
1074
1090
 
1075
1091
  #streamingEditFileCache = new Map<string, string>();
1076
1092
  #promptInFlightCount = 0;
1093
+ #abortInProgress = false;
1077
1094
  // Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
1078
1095
  // Internal extension hooks and post-emit work (auto-retry, auto-compaction, todo
1079
1096
  // checks in #handleAgentEvent) still fire on the original schedule — only the
@@ -1143,24 +1160,21 @@ export class AgentSession {
1143
1160
  }
1144
1161
  }
1145
1162
 
1146
- /** A steer/follow-up can land after the agent loop's final queue poll but
1147
- * before the prompt unwinds: #promptInFlightCount keeps isStreaming true
1148
- * through post-prompt recovery, so senders (collab guests, skills) still
1149
- * queue via agent.steer()/followUp() instead of starting a fresh prompt.
1150
- * Without a drain those messages strand invisibly until the next manual
1151
- * prompt. Runs when the session settles; the guard makes it a no-op when
1152
- * the queue was consumed normally or a new turn already started. */
1163
+ /** A steer/follow-up can land after the agent loop's final queue poll, or
1164
+ * after an abort stops an auto-continued queued turn. In both cases the
1165
+ * agent-core queue still owns the message, but no loop is left to poll it.
1166
+ * Runs whenever the session settles; the guard makes it a no-op when the
1167
+ * queue was consumed normally or a new turn already started. */
1153
1168
  #drainStrandedQueuedMessages(): void {
1154
- if (!this.agent.hasQueuedMessages()) return;
1155
- this.#scheduleAgentContinue({
1156
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
1157
- });
1169
+ if (this.#abortInProgress) return;
1170
+ this.#scheduleQueuedMessageDrain();
1158
1171
  }
1159
1172
 
1160
1173
  #resetInFlight(): void {
1161
1174
  this.#promptInFlightCount = 0;
1162
1175
  this.#releasePowerAssertion();
1163
1176
  this.#flushPendingAgentEnd();
1177
+ this.#drainStrandedQueuedMessages();
1164
1178
  }
1165
1179
 
1166
1180
  #flushPendingAgentEnd(): void {
@@ -1287,6 +1301,7 @@ export class AgentSession {
1287
1301
  this.#ttsrManager = config.ttsrManager;
1288
1302
  this.#obfuscator = config.obfuscator;
1289
1303
  this.#agentId = config.agentId;
1304
+ this.#agentKind = config.agentKind ?? "main";
1290
1305
  this.#providerSessionId = config.providerSessionId;
1291
1306
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1292
1307
  const event: AgentEvent = {
@@ -1363,7 +1378,12 @@ export class AgentSession {
1363
1378
 
1364
1379
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1365
1380
  nextToolChoice(): ToolChoice | undefined {
1366
- return this.#toolChoiceQueue.nextToolChoice();
1381
+ const choice = this.#toolChoiceQueue.nextToolChoice();
1382
+ if (isToolChoiceActive(choice, this.agent.state.tools)) {
1383
+ return choice;
1384
+ }
1385
+ this.#toolChoiceQueue.reject("unavailable");
1386
+ return undefined;
1367
1387
  }
1368
1388
 
1369
1389
  /**
@@ -1471,28 +1491,6 @@ export class AgentSession {
1471
1491
  this.#planCompactAbortPending = false;
1472
1492
  }
1473
1493
 
1474
- /** Register a compact display string for a custom message that the caller is
1475
- * about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
1476
- * Returns a stable tag the caller MUST embed in
1477
- * `CustomMessage.details.__pendingDisplayTag` so the agent-side
1478
- * `message_start` handler can remove the matching display entry when the
1479
- * queued message is consumed.
1480
- *
1481
- * Does NOT push to the agent's steering/followUp queue — that happens
1482
- * separately inside `sendCustomMessage`. */
1483
- enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
1484
- const tag = `omp-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
1485
- const displayText = text.trim();
1486
- if (!displayText) return tag;
1487
- const entry: QueuedDisplayEntry = { text: displayText, tag };
1488
- if (mode === "steer") {
1489
- this.#steeringMessages.push(entry);
1490
- } else {
1491
- this.#followUpMessages.push(entry);
1492
- }
1493
- return tag;
1494
- }
1495
-
1496
1494
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
1497
1495
  const manager = this.#asyncJobManager;
1498
1496
  if (!manager) return null;
@@ -1614,45 +1612,6 @@ export class AgentSession {
1614
1612
 
1615
1613
  /** Internal handler for agent events - shared by subscribe and reconnect */
1616
1614
  #handleAgentEvent = async (event: AgentEvent): Promise<void> => {
1617
- // When a user message starts, check if it's from either queue and remove it BEFORE emitting
1618
- // This ensures the UI sees the updated queue state
1619
- if (event.type === "message_start" && event.message.role === "user") {
1620
- const messageText = this.#getUserMessageText(event.message);
1621
- if (messageText) {
1622
- // Check steering queue first (match by .text on tagged records)
1623
- const steeringIndex = this.#steeringMessages.findIndex(e => e.text === messageText);
1624
- if (steeringIndex !== -1) {
1625
- this.#steeringMessages.splice(steeringIndex, 1);
1626
- } else {
1627
- // Check follow-up queue
1628
- const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
1629
- if (followUpIndex !== -1) {
1630
- this.#followUpMessages.splice(followUpIndex, 1);
1631
- }
1632
- }
1633
- }
1634
- }
1635
-
1636
- // Tag-based dequeue for custom messages (skills queued via promptCustomMessage).
1637
- // The InputController attached a stable tag via CustomMessage.details when it
1638
- // registered the display chip; pull it back here to remove the matching entry
1639
- // from the pending bar atomically with the agent's queue consumption. Match by
1640
- // tag (not text) — two queued skills with identical args cannot collide.
1641
- if (event.type === "message_start" && event.message.role === "custom") {
1642
- const tag = readPendingDisplayTag(event.message.details);
1643
- if (tag) {
1644
- const steerIdx = this.#steeringMessages.findIndex(e => e.tag === tag);
1645
- if (steerIdx !== -1) {
1646
- this.#steeringMessages.splice(steerIdx, 1);
1647
- } else {
1648
- const followUpIdx = this.#followUpMessages.findIndex(e => e.tag === tag);
1649
- if (followUpIdx !== -1) {
1650
- this.#followUpMessages.splice(followUpIdx, 1);
1651
- }
1652
- }
1653
- }
1654
- }
1655
-
1656
1615
  // Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
1657
1616
  // persisted message BEFORE the obfuscator's display-side copy below.
1658
1617
  // Invariant (must hold across refactors): this branch precedes the
@@ -1962,6 +1921,11 @@ export class AgentSession {
1962
1921
  return;
1963
1922
  }
1964
1923
 
1924
+ // A deliberate abort should settle the current turn, not trigger queued continuations.
1925
+ if (msg.stopReason === "aborted") {
1926
+ this.#resolveRetry();
1927
+ return;
1928
+ }
1965
1929
  // Check for retryable errors first (overloaded, rate limit, server errors)
1966
1930
  if (this.#isRetryableError(msg)) {
1967
1931
  const didRetry = await this.#handleRetryableError(msg);
@@ -1984,7 +1948,7 @@ export class AgentSession {
1984
1948
  if (compactionDeferredHandoff) {
1985
1949
  return;
1986
1950
  }
1987
- if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
1951
+ if (msg.stopReason !== "error") {
1988
1952
  if (this.#enforceRewindBeforeYield()) {
1989
1953
  return;
1990
1954
  }
@@ -2080,13 +2044,13 @@ export class AgentSession {
2080
2044
  onError?: () => void;
2081
2045
  }): void {
2082
2046
  this.#schedulePostPromptTask(
2083
- async () => {
2047
+ async signal => {
2084
2048
  // Defense in depth: if compaction/handoff slipped onto the post-prompt queue
2085
2049
  // alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
2086
2050
  // streaming turn — agent.continue() here would race the handoff's session
2087
2051
  // reset. The first-class fix is in #checkCompaction/the agent_end handler,
2088
2052
  // but this guard catches anything that bypasses that path.
2089
- if (this.isCompacting || this.isGeneratingHandoff) {
2053
+ if (signal.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) {
2090
2054
  options?.onSkip?.();
2091
2055
  return;
2092
2056
  }
@@ -2094,14 +2058,21 @@ export class AgentSession {
2094
2058
  options?.onSkip?.();
2095
2059
  return;
2096
2060
  }
2061
+ this.#beginInFlight();
2097
2062
  try {
2098
2063
  await this.#maybeRestoreRetryFallbackPrimary();
2064
+ if (signal.aborted || this.#isDisposed) {
2065
+ options?.onSkip?.();
2066
+ return;
2067
+ }
2099
2068
  await this.agent.continue();
2100
2069
  } catch (error) {
2101
2070
  logger.warn("agent.continue failed after scheduling", {
2102
2071
  error: error instanceof Error ? error.message : String(error),
2103
2072
  });
2104
2073
  options?.onError?.();
2074
+ } finally {
2075
+ this.#endInFlight();
2105
2076
  }
2106
2077
  },
2107
2078
  {
@@ -2157,8 +2128,13 @@ export class AgentSession {
2157
2128
  * and fire-and-forget `agent.continue()` may still be streaming after
2158
2129
  * the TTSR resume gate resolves.
2159
2130
  */
2160
- async #waitForPostPromptRecovery(): Promise<void> {
2131
+ async #waitForPostPromptRecovery(generation?: number): Promise<void> {
2161
2132
  while (true) {
2133
+ // An abort bumps #promptGeneration. When this wait runs on behalf of a
2134
+ // specific prompt turn, stop as soon as that turn has been superseded:
2135
+ // its promise must resolve on the abort, not block on a queued
2136
+ // steer/follow-up that the post-abort drain starts as a fresh turn.
2137
+ if (generation !== undefined && this.#promptGeneration !== generation) return;
2162
2138
  if (this.#retryPromise) {
2163
2139
  await this.#retryPromise;
2164
2140
  continue;
@@ -2620,18 +2596,6 @@ export class AgentSession {
2620
2596
 
2621
2597
  return Array.from(candidates);
2622
2598
  }
2623
- /** Extract text content from a message */
2624
- #getUserMessageText(message: Message): string {
2625
- if (message.role !== "user") return "";
2626
- const content = message.content;
2627
- if (typeof content === "string") return content;
2628
- const textBlocks = content.filter(c => c.type === "text");
2629
- const text = textBlocks.map(c => (c as TextContent).text).join("");
2630
- if (text.length > 0) return text;
2631
- const hasImages = content.some(c => c.type === "image");
2632
- return hasImages ? "[Image]" : "";
2633
- }
2634
-
2635
2599
  /** Find the last assistant message in agent state (including aborted ones) */
2636
2600
  #findLastAssistantMessage(): AssistantMessage | undefined {
2637
2601
  const messages = this.agent.state.messages;
@@ -3217,8 +3181,9 @@ export class AgentSession {
3217
3181
  // session's dispose.
3218
3182
  this.abortRetry();
3219
3183
  this.abortCompaction();
3184
+ const postPromptDrain = this.#cancelPostPromptTasks();
3220
3185
  this.agent.abort();
3221
- await this.#cancelPostPromptTasks();
3186
+ await postPromptDrain;
3222
3187
  // Cancel jobs this agent registered so a subagent's teardown doesn't
3223
3188
  // leak its background bash/task work into the parent's manager. Only
3224
3189
  // the session that owns the manager goes on to dispose it (which itself
@@ -3340,6 +3305,10 @@ export class AgentSession {
3340
3305
  return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
3341
3306
  }
3342
3307
 
3308
+ get isAborting(): boolean {
3309
+ return this.agent.isAborting;
3310
+ }
3311
+
3343
3312
  /** Wait until streaming and deferred recovery work are fully settled. */
3344
3313
  async waitForIdle(): Promise<void> {
3345
3314
  await this.agent.waitForIdle();
@@ -4527,13 +4496,17 @@ export class AgentSession {
4527
4496
  };
4528
4497
  }
4529
4498
 
4499
+ #normalizeImagesForModel(images: ImageContent[] | undefined): Promise<ImageContent[] | undefined> {
4500
+ return normalizeModelContextImages(images, { model: this.model });
4501
+ }
4502
+
4530
4503
  async #normalizeMessageContentImages(
4531
4504
  content: string | (TextContent | ImageContent)[],
4532
4505
  ): Promise<string | (TextContent | ImageContent)[]> {
4533
4506
  if (typeof content === "string") return content;
4534
4507
  const images = content.filter((part): part is ImageContent => part.type === "image");
4535
4508
  if (images.length === 0) return content;
4536
- const normalizedImages = await normalizeModelContextImages(images);
4509
+ const normalizedImages = await this.#normalizeImagesForModel(images);
4537
4510
  if (!normalizedImages) return content;
4538
4511
  let imageIndex = 0;
4539
4512
  return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
@@ -4646,9 +4619,9 @@ export class AgentSession {
4646
4619
  throw new AgentBusyError();
4647
4620
  }
4648
4621
  if (options.streamingBehavior === "followUp") {
4649
- await this.#queueFollowUp(expandedText, options?.images);
4622
+ await this.#queueUserMessage(expandedText, options?.images, "followUp");
4650
4623
  } else {
4651
- await this.#queueSteer(expandedText, options?.images);
4624
+ await this.#queueUserMessage(expandedText, options?.images, "steer");
4652
4625
  }
4653
4626
  // Steer/follow-up the keyword notices alongside the queued user message.
4654
4627
  for (const notice of keywordNotices) {
@@ -4657,11 +4630,13 @@ export class AgentSession {
4657
4630
  return true;
4658
4631
  }
4659
4632
 
4660
- // Skip eager todo prelude when the user has already queued a directive
4633
+ // Skip eager preludes when the user has already queued a directive
4661
4634
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4662
4635
  const eagerTodoPrelude =
4663
4636
  !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
4664
- const normalizedImages = await normalizeModelContextImages(options?.images);
4637
+ const eagerTaskPrelude =
4638
+ !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTaskPrelude(expandedText) : undefined;
4639
+ const normalizedImages = await this.#normalizeImagesForModel(options?.images);
4665
4640
 
4666
4641
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
4667
4642
  if (normalizedImages) {
@@ -4673,17 +4648,24 @@ export class AgentSession {
4673
4648
  ? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
4674
4649
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
4675
4650
 
4651
+ const preludeMessages: AgentMessage[] = [];
4676
4652
  if (eagerTodoPrelude) {
4677
- this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4678
- label: "eager-todo",
4679
- });
4653
+ if (eagerTodoPrelude.toolChoice) {
4654
+ this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4655
+ label: "eager-todo",
4656
+ });
4657
+ }
4658
+ preludeMessages.push(eagerTodoPrelude.message);
4659
+ }
4660
+ if (eagerTaskPrelude) {
4661
+ preludeMessages.push(eagerTaskPrelude);
4680
4662
  }
4681
4663
 
4682
4664
  try {
4683
4665
  await this.#promptWithMessage(message, expandedText, {
4684
4666
  ...options,
4685
4667
  images: normalizedImages,
4686
- prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
4668
+ prependMessages: preludeMessages.length > 0 ? preludeMessages : undefined,
4687
4669
  appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4688
4670
  });
4689
4671
  } finally {
@@ -4699,7 +4681,7 @@ export class AgentSession {
4699
4681
 
4700
4682
  async promptCustomMessage<T = unknown>(
4701
4683
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
4702
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
4684
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & { queueChipText?: string },
4703
4685
  ): Promise<void> {
4704
4686
  const textContent =
4705
4687
  typeof message.content === "string"
@@ -4723,7 +4705,10 @@ export class AgentSession {
4723
4705
  if (!options?.streamingBehavior) {
4724
4706
  throw new AgentBusyError();
4725
4707
  }
4726
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
4708
+ await this.sendCustomMessage(message, {
4709
+ deliverAs: options.streamingBehavior,
4710
+ queueChipText: options.queueChipText,
4711
+ });
4727
4712
  for (const notice of keywordNotices) {
4728
4713
  await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4729
4714
  }
@@ -4908,7 +4893,7 @@ export class AgentSession {
4908
4893
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
4909
4894
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
4910
4895
  if (!options?.skipPostPromptRecoveryWait) {
4911
- await this.#waitForPostPromptRecovery();
4896
+ await this.#waitForPostPromptRecovery(generation);
4912
4897
  }
4913
4898
  } finally {
4914
4899
  this.#endInFlight();
@@ -4958,6 +4943,7 @@ export class AgentSession {
4958
4943
  sessionManager: this.sessionManager,
4959
4944
  modelRegistry: this.#modelRegistry,
4960
4945
  model: this.model ?? undefined,
4946
+ models: createExtensionModelQuery(this.#modelRegistry, this.settings, () => this.model ?? undefined),
4961
4947
  isIdle: () => !this.isStreaming,
4962
4948
  abort: () => {
4963
4949
  void this.abort();
@@ -5060,7 +5046,7 @@ export class AgentSession {
5060
5046
  }
5061
5047
 
5062
5048
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5063
- await this.#queueSteer(expandedText, images);
5049
+ await this.#queueUserMessage(expandedText, images, "steer");
5064
5050
  }
5065
5051
 
5066
5052
  /**
@@ -5072,80 +5058,74 @@ export class AgentSession {
5072
5058
  }
5073
5059
 
5074
5060
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5075
- await this.#queueFollowUp(expandedText, images);
5061
+ await this.#queueUserMessage(expandedText, images, "followUp");
5076
5062
  }
5077
5063
 
5078
- /**
5079
- * Internal: Queue a steering message (already expanded, no extension command check).
5080
- */
5081
- async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
5082
- const normalizedImages = await normalizeModelContextImages(images);
5083
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5084
- this.#steeringMessages.push({ text: displayText, images });
5064
+ async #queueUserMessage(
5065
+ text: string,
5066
+ images: ImageContent[] | undefined,
5067
+ mode: "steer" | "followUp",
5068
+ ): Promise<void> {
5069
+ const normalizedImages = await this.#normalizeImagesForModel(images);
5085
5070
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5086
- if (normalizedImages && normalizedImages.length > 0) {
5071
+ if (normalizedImages?.length) {
5087
5072
  content.push(...normalizedImages);
5088
5073
  }
5089
- this.agent.steer({
5090
- role: "user",
5091
- content,
5092
- steering: true,
5093
- attribution: "user",
5094
- timestamp: Date.now(),
5095
- });
5096
- // A steer can land on an idle session: the caller checked isStreaming
5097
- // before the (potentially slow) image normalization above, so the turn
5098
- // may have ended in between. Without a drain the message would strand in
5099
- // the queue until the next manual prompt — schedule an immediate continue,
5100
- // mirroring #queueFollowUp's idle-path delivery.
5101
- if (this.#canAutoContinueForFollowUp()) {
5102
- this.#scheduleAgentContinue({
5103
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5074
+ if (mode === "followUp") {
5075
+ this.agent.followUp({
5076
+ role: "user",
5077
+ content,
5078
+ attribution: "user",
5079
+ timestamp: Date.now(),
5080
+ });
5081
+ } else {
5082
+ this.agent.steer({
5083
+ role: "user",
5084
+ content,
5085
+ steering: true,
5086
+ attribution: "user",
5087
+ timestamp: Date.now(),
5104
5088
  });
5105
5089
  }
5090
+ this.#scheduleIdleQueueDrain();
5106
5091
  }
5107
5092
 
5108
- /**
5109
- * Internal: Queue a follow-up message (already expanded, no extension command check).
5110
- */
5111
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5112
- const normalizedImages = await normalizeModelContextImages(images);
5113
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5114
- this.#followUpMessages.push({ text: displayText, images });
5115
- const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5116
- if (normalizedImages && normalizedImages.length > 0) {
5117
- content.push(...normalizedImages);
5093
+ #scheduleIdleQueueDrain(): void {
5094
+ this.#scheduleQueuedMessageDrain();
5095
+ }
5096
+
5097
+ #scheduleQueuedMessageDrain(): void {
5098
+ if (this.#queuedMessageDrainScheduled || !this.#canAutoContinueForFollowUp() || !this.agent.hasQueuedMessages()) {
5099
+ return;
5118
5100
  }
5119
- this.agent.followUp({
5120
- role: "user",
5121
- content,
5122
- attribution: "user",
5123
- timestamp: Date.now(),
5101
+ this.#queuedMessageDrainScheduled = true;
5102
+ this.#scheduleAgentContinue({
5103
+ shouldContinue: () => {
5104
+ this.#queuedMessageDrainScheduled = false;
5105
+ return this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages();
5106
+ },
5107
+ onSkip: () => {
5108
+ this.#queuedMessageDrainScheduled = false;
5109
+ },
5110
+ onError: () => {
5111
+ this.#queuedMessageDrainScheduled = false;
5112
+ },
5124
5113
  });
5125
- // When fully idle AND the session is in a resumable assistant-ended state,
5126
- // schedule an immediate continue so the queued follow-up is delivered
5127
- // without waiting for the next user turn. We gate on isStreaming (model
5128
- // actively producing), isRetrying (auto-retry backoff is sleeping between
5129
- // attempts, #retryPromise set), and the last message being assistant —
5130
- // agent.continue() only dequeues follow-ups from an assistant-ended state;
5131
- // resuming from user/toolResult state runs an extra model call on the
5132
- // stale prompt before draining the queue.
5133
- if (this.#canAutoContinueForFollowUp()) {
5134
- this.#scheduleAgentContinue({
5135
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5136
- });
5137
- }
5138
5114
  }
5139
5115
 
5140
5116
  /**
5141
- * Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
5117
+ * Gate for idle-path queued-message auto-continue. See `#scheduleIdleQueueDrain` for rationale.
5142
5118
  */
5143
5119
  #canAutoContinueForFollowUp(): boolean {
5144
5120
  if (this.isStreaming) return false;
5145
5121
  if (this.isRetrying) return false;
5146
5122
  const messages = this.agent.state.messages;
5147
5123
  const last = messages[messages.length - 1];
5148
- return last?.role === "assistant";
5124
+ // A user interrupt during tool execution can leave the transcript ending
5125
+ // with the emitted tool result, not the aborted assistant message. Continuing
5126
+ // from that state is still resumable: Agent.continue() first polls queued
5127
+ // steering before making the next model call.
5128
+ return last?.role === "assistant" || last?.role === "toolResult";
5149
5129
  }
5150
5130
 
5151
5131
  queueDeferredMessage(message: CustomMessage): void {
@@ -5244,17 +5224,33 @@ export class AgentSession {
5244
5224
  * - Streaming: queue as steer/follow-up or store for next turn
5245
5225
  * - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it
5246
5226
  * - Not streaming + no trigger: appends to state/session, no turn
5227
+ *
5228
+ * @returns true iff this call synchronously started a new turn (awaited
5229
+ * `agent.prompt`); false when the message was queued/appended without a turn
5230
+ * — including when `triggerTurn` is downgraded because the client defers
5231
+ * agent-initiated turns. Callers that must mirror the resulting `agent_end`
5232
+ * use this to avoid acting on a turn that never ran.
5247
5233
  */
5248
5234
  async sendCustomMessage<T = unknown>(
5249
5235
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5250
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
5251
- ): Promise<void> {
5236
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
5237
+ ): Promise<boolean> {
5238
+ const details =
5239
+ options?.queueChipText && options.deliverAs !== "nextTurn"
5240
+ ? ({
5241
+ ...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
5242
+ string,
5243
+ unknown
5244
+ >),
5245
+ __queueChipText: options.queueChipText,
5246
+ } as T)
5247
+ : message.details;
5252
5248
  const appMessage: CustomMessage<T> = {
5253
5249
  role: "custom",
5254
5250
  customType: message.customType,
5255
5251
  content: message.content,
5256
5252
  display: message.display,
5257
- details: message.details,
5253
+ details,
5258
5254
  attribution: message.attribution ?? "agent",
5259
5255
  timestamp: Date.now(),
5260
5256
  };
@@ -5262,7 +5258,7 @@ export class AgentSession {
5262
5258
  if (this.isStreaming) {
5263
5259
  if (options?.deliverAs === "nextTurn") {
5264
5260
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
5265
- return;
5261
+ return false;
5266
5262
  }
5267
5263
 
5268
5264
  if (options?.deliverAs === "followUp") {
@@ -5270,25 +5266,18 @@ export class AgentSession {
5270
5266
  } else {
5271
5267
  this.agent.steer(normalizedAppMessage);
5272
5268
  }
5273
- // The isStreaming check above can be stale: image normalization is
5274
- // awaited, so the turn may have ended in between, leaving the message
5275
- // queued on an idle agent. Mirror #queueSteer's idle-path delivery.
5276
- if (this.#canAutoContinueForFollowUp()) {
5277
- this.#scheduleAgentContinue({
5278
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5279
- });
5280
- }
5281
- return;
5269
+ this.#scheduleIdleQueueDrain();
5270
+ return false;
5282
5271
  }
5283
5272
 
5284
5273
  if (options?.deliverAs === "nextTurn") {
5285
5274
  if (options?.triggerTurn) {
5286
5275
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
5287
5276
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5288
- return;
5277
+ return false;
5289
5278
  }
5290
5279
  await this.agent.prompt(normalizedAppMessage);
5291
- return;
5280
+ return true;
5292
5281
  }
5293
5282
  this.agent.appendMessage(normalizedAppMessage);
5294
5283
  this.sessionManager.appendCustomMessageEntry(
@@ -5298,16 +5287,16 @@ export class AgentSession {
5298
5287
  message.details,
5299
5288
  message.attribution ?? "agent",
5300
5289
  );
5301
- return;
5290
+ return false;
5302
5291
  }
5303
5292
 
5304
5293
  if (options?.triggerTurn) {
5305
5294
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
5306
5295
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5307
- return;
5296
+ return false;
5308
5297
  }
5309
5298
  await this.agent.prompt(normalizedAppMessage);
5310
- return;
5299
+ return true;
5311
5300
  }
5312
5301
 
5313
5302
  this.agent.appendMessage(normalizedAppMessage);
@@ -5318,6 +5307,7 @@ export class AgentSession {
5318
5307
  message.details,
5319
5308
  message.attribution ?? "agent",
5320
5309
  );
5310
+ return false;
5321
5311
  }
5322
5312
 
5323
5313
  /**
@@ -5352,11 +5342,11 @@ export class AgentSession {
5352
5342
  }
5353
5343
 
5354
5344
  if (options?.deliverAs === "followUp") {
5355
- await this.#queueFollowUp(text, images);
5345
+ await this.#queueUserMessage(text, images, "followUp");
5356
5346
  return;
5357
5347
  }
5358
5348
  if (options?.deliverAs === "steer") {
5359
- await this.#queueSteer(text, images);
5349
+ await this.#queueUserMessage(text, images, "steer");
5360
5350
  return;
5361
5351
  }
5362
5352
 
@@ -5367,57 +5357,37 @@ export class AgentSession {
5367
5357
  });
5368
5358
  }
5369
5359
 
5370
- /**
5371
- * Clear queued messages and return them (text plus any attached images).
5372
- * Useful for restoring to editor when user aborts. The internal entry
5373
- * arrays are handed out as-is — a `tag` (if any) is inert once the record
5374
- * leaves the queue.
5375
- */
5360
+ /** Clear queued messages and return them (text plus any attached images). */
5376
5361
  clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
5377
- const steering = this.#steeringMessages;
5378
- const followUp = this.#followUpMessages;
5379
- this.#steeringMessages = [];
5380
- this.#followUpMessages = [];
5362
+ const steering = this.agent.peekSteeringQueue().map(toRestoredQueuedMessage);
5363
+ const followUp = this.agent.peekFollowUpQueue().map(toRestoredQueuedMessage);
5381
5364
  this.agent.clearAllQueues();
5382
5365
  return { steering, followUp };
5383
5366
  }
5384
5367
 
5385
- /** Number of pending messages (includes steering, follow-up, and next-turn messages) */
5368
+ /** Number of pending displayable messages (includes steering, follow-up, and next-turn messages) */
5386
5369
  get queuedMessageCount(): number {
5387
- return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
5370
+ return (
5371
+ this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
5372
+ this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).length +
5373
+ this.#pendingNextTurnMessages.length
5374
+ );
5388
5375
  }
5389
5376
 
5390
- /** Get pending messages (read-only). Returns the public text-only view;
5391
- * internal `{text, tag?}` records are mapped to `.text` so callers
5392
- * (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
5393
- * the unchanged historical shape. */
5394
5377
  getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
5395
5378
  return {
5396
- steering: this.#steeringMessages.map(e => e.text),
5397
- followUp: this.#followUpMessages.map(e => e.text),
5379
+ steering: this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5380
+ followUp: this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5398
5381
  };
5399
5382
  }
5400
5383
 
5401
5384
  /**
5402
5385
  * Pop the last queued message (steering first, then follow-up).
5403
5386
  * Used by dequeue keybinding to restore messages to editor one at a time.
5404
- * Returns the popped entry's text and images; the tag (if any) dies with
5405
- * the record — no orphan state can outlive the queue entry.
5406
5387
  */
5407
5388
  popLastQueuedMessage(): RestoredQueuedMessage | undefined {
5408
- // Pop from steering first (LIFO)
5409
- if (this.#steeringMessages.length > 0) {
5410
- const entry = this.#steeringMessages.pop();
5411
- this.agent.popLastSteer();
5412
- return entry;
5413
- }
5414
- // Then from follow-up
5415
- if (this.#followUpMessages.length > 0) {
5416
- const entry = this.#followUpMessages.pop();
5417
- this.agent.popLastFollowUp();
5418
- return entry;
5419
- }
5420
- return undefined;
5389
+ const message = this.agent.popLastSteer() ?? this.agent.popLastFollowUp();
5390
+ return message ? toRestoredQueuedMessage(message) : undefined;
5421
5391
  }
5422
5392
 
5423
5393
  get skillsSettings(): SkillsSettings | undefined {
@@ -5474,44 +5444,40 @@ export class AgentSession {
5474
5444
  * abort. Omit it for internal/lifecycle aborts.
5475
5445
  */
5476
5446
  async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5477
- this.abortRetry();
5478
- this.#promptGeneration++;
5479
- this.#scheduledHiddenNextTurnGeneration = undefined;
5480
- this.abortCompaction();
5481
- this.abortHandoff();
5482
- this.abortBash();
5483
- this.abortEval();
5484
- const postPromptDrain = this.#cancelPostPromptTasks();
5485
- this.agent.abort(options?.reason);
5486
- await postPromptDrain;
5487
- await this.agent.waitForIdle();
5488
- await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5489
- // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5490
- // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5491
- // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5492
- this.#resetInFlight();
5493
- // Safety net: if the agent loop aborted without producing an assistant
5494
- // message (e.g. failed before the first stream), the in-flight yield was
5495
- // never resolved or rejected by the normal message_end path. Reject it now
5496
- // so any requeue callback still fires and the queue stays consistent.
5497
- if (this.#toolChoiceQueue.hasInFlight) {
5498
- this.#toolChoiceQueue.reject("aborted");
5447
+ // Session switch/compact paths disconnect first; explicit aborts should
5448
+ // leave any queued steer/follow-up visible for the user rather than
5449
+ // auto-starting a fresh turn during cleanup.
5450
+ this.#abortInProgress = true;
5451
+ try {
5452
+ this.abortRetry();
5453
+ this.#promptGeneration++;
5454
+ this.#scheduledHiddenNextTurnGeneration = undefined;
5455
+ this.abortCompaction();
5456
+ this.abortHandoff();
5457
+ this.abortBash();
5458
+ this.abortEval();
5459
+ const postPromptDrain = this.#cancelPostPromptTasks();
5460
+ this.agent.abort(options?.reason);
5461
+ await postPromptDrain;
5462
+ await this.agent.waitForIdle();
5463
+ await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5464
+ // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5465
+ // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5466
+ // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5467
+ this.#resetInFlight();
5468
+ // Safety net: if the agent loop aborted without producing an assistant
5469
+ // message (e.g. failed before the first stream), the in-flight yield was
5470
+ // never resolved or rejected by the normal message_end path. Reject it now
5471
+ // so any requeue callback still fires and the queue stays consistent.
5472
+ if (this.#toolChoiceQueue.hasInFlight) {
5473
+ this.#toolChoiceQueue.reject("aborted");
5474
+ }
5475
+ } finally {
5476
+ this.#abortInProgress = false;
5477
+ this.#drainStrandedQueuedMessages();
5499
5478
  }
5500
5479
  }
5501
5480
 
5502
- /**
5503
- * Abort active work, then immediately resume the agent so queued steer/follow-up
5504
- * messages drain instead of waiting for another natural turn boundary.
5505
- */
5506
- async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
5507
- if (!this.agent.hasQueuedMessages()) return;
5508
- await this.abort({ reason: options?.reason });
5509
- if (!this.agent.hasQueuedMessages()) return;
5510
- if (this.isCompacting || this.isGeneratingHandoff) return;
5511
- await this.#maybeRestoreRetryFallbackPrimary();
5512
- await this.agent.continue();
5513
- }
5514
-
5515
5481
  /**
5516
5482
  * Start a new session, optionally with initial messages and parent tracking.
5517
5483
  * Clears all messages and starts a new session.
@@ -5562,8 +5528,6 @@ export class AgentSession {
5562
5528
  this.#rekeyMnemopiMemoryForCurrentSessionId();
5563
5529
  this.#resetHindsightConversationTrackingIfHindsight();
5564
5530
  this.#resetMnemopiConversationTrackingIfMnemopi();
5565
- this.#steeringMessages = [];
5566
- this.#followUpMessages = [];
5567
5531
  this.#pendingNextTurnMessages = [];
5568
5532
  this.#scheduledHiddenNextTurnGeneration = undefined;
5569
5533
 
@@ -5678,7 +5642,10 @@ export class AgentSession {
5678
5642
 
5679
5643
  /**
5680
5644
  * Set model directly.
5681
- * Validates API key and saves to the active session. Persists settings only when requested.
5645
+ * Validates that a credential source is configured (synchronously, without
5646
+ * refreshing OAuth or running command-backed key programs) and saves to the
5647
+ * active session. Persists settings only when requested. The concrete key is
5648
+ * resolved lazily per request, so switching never blocks the event loop.
5682
5649
  * @throws Error if no API key available for the model
5683
5650
  */
5684
5651
  async setModel(
@@ -5687,8 +5654,7 @@ export class AgentSession {
5687
5654
  options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5688
5655
  ): Promise<void> {
5689
5656
  const previousEditMode = this.#resolveActiveEditMode();
5690
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5691
- if (!apiKey) {
5657
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5692
5658
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5693
5659
  }
5694
5660
 
@@ -5711,7 +5677,9 @@ export class AgentSession {
5711
5677
 
5712
5678
  /**
5713
5679
  * Set model temporarily (for this session only).
5714
- * Validates API key, saves to session log but NOT to settings.
5680
+ * Validates that a credential source is configured (synchronously, without
5681
+ * refreshing OAuth or running command-backed key programs), saves to session
5682
+ * log but NOT to settings.
5715
5683
  * @throws Error if no API key available for the model
5716
5684
  */
5717
5685
  async setModelTemporary(
@@ -5720,8 +5688,7 @@ export class AgentSession {
5720
5688
  options?: { ephemeral?: boolean },
5721
5689
  ): Promise<void> {
5722
5690
  const previousEditMode = this.#resolveActiveEditMode();
5723
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5724
- if (!apiKey) {
5691
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5725
5692
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5726
5693
  }
5727
5694
 
@@ -6113,7 +6080,12 @@ export class AgentSession {
6113
6080
  // Already on under any scope — keep the user's scoped value.
6114
6081
  return;
6115
6082
  }
6116
- this.setServiceTier(enabled ? "priority" : undefined);
6083
+ if (!enabled) {
6084
+ this.setServiceTier(undefined);
6085
+ return;
6086
+ }
6087
+ const scope = this.settings.get("fastModeScope");
6088
+ this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
6117
6089
  }
6118
6090
 
6119
6091
  toggleFastMode(): boolean {
@@ -6705,8 +6677,6 @@ export class AgentSession {
6705
6677
  this.#rekeyMnemopiMemoryForCurrentSessionId();
6706
6678
  this.#resetHindsightConversationTrackingIfHindsight();
6707
6679
  this.#resetMnemopiConversationTrackingIfMnemopi();
6708
- this.#steeringMessages = [];
6709
- this.#followUpMessages = [];
6710
6680
  this.#pendingNextTurnMessages = [];
6711
6681
  this.#scheduledHiddenNextTurnGeneration = undefined;
6712
6682
  this.#todoReminderCount = 0;
@@ -6978,10 +6948,11 @@ export class AgentSession {
6978
6948
  #isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
6979
6949
  switch (assistantMessage.stopReason) {
6980
6950
  case "stop":
6951
+ // Reasoning/thinking-only turns are not actionable: they do not
6952
+ // answer the user and do not give the agent loop a tool call to run.
6981
6953
  for (const content of assistantMessage.content) {
6982
6954
  if (content.type === "toolCall") return false;
6983
6955
  if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6984
- if (content.type === "thinking" && hasNonWhitespace(content.thinking)) return false;
6985
6956
  }
6986
6957
  return true;
6987
6958
  case "toolUse":
@@ -7132,10 +7103,28 @@ export class AgentSession {
7132
7103
  });
7133
7104
  }
7134
7105
 
7135
- #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
7136
- const eagerTodosEnabled = this.settings.get("todo.eager");
7106
+ /**
7107
+ * Render context shared by the eager todo/task preludes. `toolRefs` resolves each
7108
+ * tool's wire name (matching `buildSystemPrompt`'s `toolRefs`) so the reminder names
7109
+ * the tool the model actually sees when an extension renames it; `taskBatch` gates
7110
+ * batch-call guidance that would steer toward a failing call shape when `task.batch`
7111
+ * is off (the flat single-spawn schema rejects `tasks`/`context`).
7112
+ */
7113
+ #buildEagerPreludeContext(): { toolRefs: Record<string, string>; taskBatch: boolean } {
7114
+ const wireName = (name: string): string => {
7115
+ const tool = this.#toolRegistry.get(name);
7116
+ return typeof tool?.customWireName === "string" ? tool.customWireName : name;
7117
+ };
7118
+ return {
7119
+ toolRefs: { task: wireName("task"), todo: wireName("todo") },
7120
+ taskBatch: this.settings.get("task.batch"),
7121
+ };
7122
+ }
7123
+
7124
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
7125
+ const mode = this.settings.get("todo.eager");
7137
7126
  const todosEnabled = this.settings.get("todo.enabled");
7138
- if (!eagerTodosEnabled || !todosEnabled) {
7127
+ if (mode === "default" || !todosEnabled) {
7139
7128
  return undefined;
7140
7129
  }
7141
7130
 
@@ -7170,27 +7159,53 @@ export class AgentSession {
7170
7159
  return undefined;
7171
7160
  }
7172
7161
 
7162
+ const message: AgentMessage = {
7163
+ role: "custom",
7164
+ customType: "eager-todo-prelude",
7165
+ content: prompt.render(eagerTodoPrompt, { ...this.#buildEagerPreludeContext(), forced: mode === "always" }),
7166
+ display: false,
7167
+ attribution: "agent",
7168
+ timestamp: Date.now(),
7169
+ };
7170
+ // `preferred` suggests a todo list (reminder only); `always` also forces the
7171
+ // `todo` tool on the first turn — the previous boolean-on behavior.
7172
+ if (mode === "preferred") {
7173
+ return { message };
7174
+ }
7173
7175
  const todoToolChoice = buildNamedToolChoice("todo", this.model);
7174
7176
  if (!todoToolChoice) {
7175
- logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo", {
7176
- modelApi: this.model?.api,
7177
- modelId: this.model?.id,
7178
- });
7179
- return undefined;
7180
- }
7181
-
7182
- const eagerTodoReminder = prompt.render(eagerTodoPrompt);
7183
-
7177
+ // `always` on a model that can't be forced degrades to reminder-only (no
7178
+ // tool_choice). For `todo.eager: true` users migrated to `always`, such
7179
+ // models now receive the first-turn reminder where they previously got
7180
+ // nothing (see the CHANGELOG entry); `always ⊇ preferred` is preserved.
7181
+ logger.warn(
7182
+ "Eager todo proceeding with the reminder only because the current model does not support a forced todo tool_choice",
7183
+ { modelApi: this.model?.api, modelId: this.model?.id },
7184
+ );
7185
+ return { message };
7186
+ }
7187
+ return { message, toolChoice: todoToolChoice };
7188
+ }
7189
+
7190
+ #createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
7191
+ if (this.settings.get("task.eager") !== "always") return undefined;
7192
+ // Main agent only: subagents keep `task` active (the parent only filters `todo`),
7193
+ // so a salient delegate-reminder there would amplify nested fan-out. Gate on the
7194
+ // resolved agent kind, not the id, so a top-level session with a custom `agentId`
7195
+ // still gets the reminder.
7196
+ if (this.#agentKind === "sub") return undefined;
7197
+ if (this.#planModeState?.enabled) return undefined;
7198
+ if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
7199
+ const trimmed = promptText.trimEnd();
7200
+ if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
7201
+ if (!this.getActiveToolNames().includes("task")) return undefined;
7184
7202
  return {
7185
- message: {
7186
- role: "custom",
7187
- customType: "eager-todo-prelude",
7188
- content: eagerTodoReminder,
7189
- display: false,
7190
- attribution: "agent",
7191
- timestamp: Date.now(),
7192
- },
7193
- toolChoice: todoToolChoice,
7203
+ role: "custom",
7204
+ customType: "eager-task-prelude",
7205
+ content: prompt.render(eagerTaskPrompt, this.#buildEagerPreludeContext()),
7206
+ display: false,
7207
+ attribution: "agent",
7208
+ timestamp: Date.now(),
7194
7209
  };
7195
7210
  }
7196
7211
  /**
@@ -7315,7 +7330,7 @@ export class AgentSession {
7315
7330
  const candidate = this.#resolveContextPromotionConfiguredTarget(currentModel, availableModels);
7316
7331
  if (!candidate) return undefined;
7317
7332
  if (modelsAreEqual(candidate, currentModel)) return undefined;
7318
- if (candidate.contextWindow <= contextWindow) return undefined;
7333
+ if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
7319
7334
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
7320
7335
  if (!apiKey) return undefined;
7321
7336
  return candidate;
@@ -7626,7 +7641,7 @@ export class AgentSession {
7626
7641
  addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
7627
7642
  }
7628
7643
 
7629
- const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
7644
+ const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
7630
7645
  for (const model of sortedByContext) {
7631
7646
  if (!seen.has(this.#getModelKey(model))) {
7632
7647
  addCandidate(model);
@@ -9603,8 +9618,8 @@ export class AgentSession {
9603
9618
  // the existing message objects is sufficient and avoids structured-clone failures for
9604
9619
  // extension/custom metadata that is valid to persist but not cloneable.
9605
9620
  const previousAgentMessages = [...this.agent.state.messages];
9606
- const previousSteeringMessages = [...this.#steeringMessages];
9607
- const previousFollowUpMessages = [...this.#followUpMessages];
9621
+ const previousSteeringMessages = [...this.agent.peekSteeringQueue()];
9622
+ const previousFollowUpMessages = [...this.agent.peekFollowUpQueue()];
9608
9623
  const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
9609
9624
  const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
9610
9625
  const previousModel = this.model;
@@ -9621,8 +9636,7 @@ export class AgentSession {
9621
9636
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
9622
9637
  : undefined;
9623
9638
 
9624
- this.#steeringMessages = [];
9625
- this.#followUpMessages = [];
9639
+ this.agent.clearAllQueues();
9626
9640
  this.#pendingNextTurnMessages = [];
9627
9641
  this.#scheduledHiddenNextTurnGeneration = undefined;
9628
9642
 
@@ -9763,8 +9777,7 @@ export class AgentSession {
9763
9777
  this.#baseSystemPrompt = previousBaseSystemPrompt;
9764
9778
  this.agent.setSystemPrompt(previousSystemPrompt);
9765
9779
  this.agent.replaceMessages(previousAgentMessages);
9766
- this.#steeringMessages = previousSteeringMessages;
9767
- this.#followUpMessages = previousFollowUpMessages;
9780
+ this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
9768
9781
  this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
9769
9782
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
9770
9783
  if (previousModel) {