@prometheus-ai/agent 0.5.4 → 0.5.8

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 (1145) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli.js +25110 -0
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/async/job-manager.d.ts +33 -0
  5. package/dist/types/autolearn/controller.d.ts +25 -0
  6. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  7. package/dist/types/autoresearch/state.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  11. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  12. package/dist/types/autoresearch/types.d.ts +1 -1
  13. package/dist/types/capability/context-file.d.ts +0 -13
  14. package/dist/types/capability/mcp.d.ts +1 -0
  15. package/dist/types/capability/rule-buckets.d.ts +1 -1
  16. package/dist/types/capability/rule.d.ts +6 -1
  17. package/dist/types/capability/types.d.ts +0 -4
  18. package/dist/types/cli/args.d.ts +23 -3
  19. package/dist/types/cli/bench-cli.d.ts +78 -0
  20. package/dist/types/cli/claude-trace-cli.d.ts +7 -0
  21. package/dist/types/cli/dry-balance-cli.d.ts +16 -2
  22. package/dist/types/cli/gallery-cli.d.ts +43 -0
  23. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  24. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  25. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  26. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  27. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  28. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  29. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  30. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  31. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  32. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  33. package/dist/types/cli/gallery-fixtures/types.d.ts +55 -0
  34. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  35. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  36. package/dist/types/cli/grievances-cli.d.ts +1 -1
  37. package/dist/types/cli/list-models.d.ts +6 -14
  38. package/dist/types/cli/models-cli.d.ts +49 -0
  39. package/dist/types/cli/session-picker.d.ts +1 -1
  40. package/dist/types/cli/setup-cli.d.ts +1 -1
  41. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  42. package/dist/types/cli/startup-cwd.d.ts +2 -0
  43. package/dist/types/cli/update-cli.d.ts +13 -40
  44. package/dist/types/cli/usage-cli.d.ts +81 -0
  45. package/dist/types/cli-commands.d.ts +12 -0
  46. package/dist/types/collab/crypto.d.ts +7 -0
  47. package/dist/types/collab/guest.d.ts +37 -0
  48. package/dist/types/collab/host.d.ts +29 -0
  49. package/dist/types/collab/protocol.d.ts +119 -0
  50. package/dist/types/collab/relay-client.d.ts +22 -0
  51. package/dist/types/commands/bench.d.ts +29 -0
  52. package/dist/types/commands/gallery.d.ts +47 -0
  53. package/dist/types/commands/install.d.ts +1 -1
  54. package/dist/types/commands/join.d.ts +12 -0
  55. package/dist/types/commands/launch.d.ts +8 -4
  56. package/dist/types/commands/models.d.ts +33 -0
  57. package/dist/types/commands/read.d.ts +1 -1
  58. package/dist/types/commands/say.d.ts +24 -0
  59. package/dist/types/commands/token.d.ts +25 -0
  60. package/dist/types/commands/usage.d.ts +34 -0
  61. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  62. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  63. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  64. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  65. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  66. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  67. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  68. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  69. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  70. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  71. package/dist/types/commit/analysis/summary.d.ts +2 -2
  72. package/dist/types/commit/changelog/generate.d.ts +3 -3
  73. package/dist/types/commit/changelog/index.d.ts +2 -2
  74. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  75. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  76. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  77. package/dist/types/commit/model-selection.d.ts +10 -4
  78. package/dist/types/commit/shared-llm.d.ts +1 -1
  79. package/dist/types/config/api-key-resolver.d.ts +43 -0
  80. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  81. package/dist/types/config/keybindings.d.ts +12 -7
  82. package/dist/types/config/model-discovery.d.ts +57 -0
  83. package/dist/types/config/model-equivalence.d.ts +1 -1
  84. package/dist/types/config/model-registry.d.ts +86 -222
  85. package/dist/types/config/model-resolver.d.ts +43 -12
  86. package/dist/types/config/model-roles.d.ts +29 -0
  87. package/dist/types/config/models-config-schema.d.ts +536 -43
  88. package/dist/types/config/models-config.d.ts +391 -0
  89. package/dist/types/config/settings-schema.d.ts +1202 -324
  90. package/dist/types/config/settings.d.ts +15 -3
  91. package/dist/types/dap/config.d.ts +14 -1
  92. package/dist/types/dap/types.d.ts +10 -0
  93. package/dist/types/debug/log-viewer.d.ts +1 -1
  94. package/dist/types/debug/raw-sse.d.ts +1 -1
  95. package/dist/types/debug/report-bundle.d.ts +3 -0
  96. package/dist/types/debug/terminal-info.d.ts +0 -1
  97. package/dist/types/discovery/at-imports.d.ts +15 -0
  98. package/dist/types/discovery/prometheus-extension-roots.d.ts +7 -7
  99. package/dist/types/edit/diff.d.ts +3 -2
  100. package/dist/types/edit/file-snapshot-store.d.ts +18 -0
  101. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  102. package/dist/types/edit/hashline/params.d.ts +1 -1
  103. package/dist/types/edit/index.d.ts +0 -1
  104. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  105. package/dist/types/edit/modes/patch.d.ts +1 -1
  106. package/dist/types/edit/modes/replace.d.ts +1 -1
  107. package/dist/types/edit/renderer.d.ts +1 -0
  108. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +1 -0
  109. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
  110. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
  111. package/dist/types/eval/backend.d.ts +7 -2
  112. package/dist/types/eval/bridge-timeout.d.ts +1 -1
  113. package/dist/types/eval/completion-bridge.d.ts +25 -0
  114. package/dist/types/eval/idle-timeout.d.ts +1 -5
  115. package/dist/types/eval/js/context-manager.d.ts +1 -0
  116. package/dist/types/eval/js/executor.d.ts +2 -0
  117. package/dist/types/eval/js/index.d.ts +1 -1
  118. package/dist/types/eval/js/shared/helpers.d.ts +7 -1
  119. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  120. package/dist/types/eval/js/shared/runtime.d.ts +6 -1
  121. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  122. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  123. package/dist/types/eval/py/executor.d.ts +12 -0
  124. package/dist/types/eval/py/index.d.ts +1 -1
  125. package/dist/types/eval/py/kernel.d.ts +6 -1
  126. package/dist/types/eval/py/runtime.d.ts +9 -0
  127. package/dist/types/exa/index.d.ts +1 -19
  128. package/dist/types/exa/mcp-client.d.ts +10 -3
  129. package/dist/types/exa/types.d.ts +0 -83
  130. package/dist/types/exec/bash-executor.d.ts +7 -0
  131. package/dist/types/export/custom-share.d.ts +1 -2
  132. package/dist/types/export/html/index.d.ts +39 -0
  133. package/dist/types/export/html/template-js.d.ts +2 -0
  134. package/dist/types/export/share.d.ts +61 -0
  135. package/dist/types/export/ttsr.d.ts +14 -0
  136. package/dist/types/extensibility/custom-commands/types.d.ts +9 -4
  137. package/dist/types/extensibility/custom-tools/loader.d.ts +30 -4
  138. package/dist/types/extensibility/custom-tools/types.d.ts +16 -8
  139. package/dist/types/extensibility/extensions/index.d.ts +1 -1
  140. package/dist/types/extensibility/extensions/loader.d.ts +20 -1
  141. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  142. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  143. package/dist/types/extensibility/extensions/types.d.ts +72 -11
  144. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  145. package/dist/types/extensibility/hooks/loader.d.ts +1 -1
  146. package/dist/types/extensibility/hooks/types.d.ts +11 -5
  147. package/dist/types/extensibility/{legacy-pi-ai-shim.d.ts → legacy-package-ai-shim.d.ts} +2 -2
  148. package/dist/types/extensibility/plugins/{legacy-pi-compat.d.ts → legacy-package-compat.d.ts} +20 -3
  149. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  150. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  151. package/dist/types/extensibility/plugins/types.d.ts +2 -2
  152. package/dist/types/extensibility/shared-events.d.ts +3 -3
  153. package/dist/types/extensibility/skills.d.ts +10 -0
  154. package/dist/types/extensibility/slash-commands.d.ts +1 -11
  155. package/dist/types/goals/guided-setup.d.ts +18 -0
  156. package/dist/types/goals/state.d.ts +1 -1
  157. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  158. package/dist/types/hindsight/mental-models.d.ts +17 -8
  159. package/dist/types/hindsight/transcript.d.ts +1 -1
  160. package/dist/types/index.d.ts +5 -0
  161. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  162. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  163. package/dist/types/internal-urls/index.d.ts +1 -0
  164. package/dist/types/internal-urls/local-protocol.d.ts +14 -2
  165. package/dist/types/internal-urls/types.d.ts +1 -1
  166. package/dist/types/irc/bus.d.ts +79 -0
  167. package/dist/types/lib/xai-http.d.ts +1 -1
  168. package/dist/types/lsp/client.d.ts +10 -0
  169. package/dist/types/lsp/config.d.ts +2 -2
  170. package/dist/types/lsp/edits.d.ts +9 -0
  171. package/dist/types/lsp/format-options.d.ts +32 -0
  172. package/dist/types/lsp/index.d.ts +2 -7
  173. package/dist/types/lsp/types.d.ts +13 -1
  174. package/dist/types/lsp/utils.d.ts +6 -2
  175. package/dist/types/main.d.ts +23 -8
  176. package/dist/types/mcp/json-rpc.d.ts +5 -0
  177. package/dist/types/mcp/manager.d.ts +8 -0
  178. package/dist/types/mcp/oauth-discovery.d.ts +6 -1
  179. package/dist/types/mcp/oauth-flow.d.ts +13 -3
  180. package/dist/types/mcp/startup-events.d.ts +11 -0
  181. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  182. package/dist/types/mcp/transports/stdio.d.ts +13 -0
  183. package/dist/types/mcp/types.d.ts +2 -0
  184. package/dist/types/memories/index.d.ts +7 -15
  185. package/dist/types/memories/storage.d.ts +0 -10
  186. package/dist/types/memory-backend/index.d.ts +3 -1
  187. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  188. package/dist/types/memory-backend/resolve.d.ts +2 -2
  189. package/dist/types/memory-backend/runtime.d.ts +4 -0
  190. package/dist/types/memory-backend/types.d.ts +67 -2
  191. package/dist/types/mnemopi/config.d.ts +31 -1
  192. package/dist/types/mnemopi/state.d.ts +40 -2
  193. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  194. package/dist/types/modes/components/agent-dashboard.d.ts +17 -1
  195. package/dist/types/modes/components/agent-hub.d.ts +82 -0
  196. package/dist/types/modes/components/assistant-message.d.ts +5 -12
  197. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  198. package/dist/types/modes/components/chat-block.d.ts +64 -0
  199. package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
  200. package/dist/types/modes/components/compaction-summary-message.d.ts +25 -5
  201. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  202. package/dist/types/modes/components/custom-editor.d.ts +49 -2
  203. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  204. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  205. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  206. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  207. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  208. package/dist/types/modes/components/footer.d.ts +1 -1
  209. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  210. package/dist/types/modes/components/hook-input.d.ts +4 -0
  211. package/dist/types/modes/components/hook-selector.d.ts +5 -7
  212. package/dist/types/modes/components/index.d.ts +1 -0
  213. package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
  214. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  215. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  216. package/dist/types/modes/components/model-selector.d.ts +1 -1
  217. package/dist/types/modes/components/oauth-selector.d.ts +10 -1
  218. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  219. package/dist/types/modes/components/plan-review-overlay.d.ts +61 -0
  220. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  221. package/dist/types/modes/components/read-tool-group.d.ts +8 -0
  222. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  223. package/dist/types/modes/components/segment-track.d.ts +11 -6
  224. package/dist/types/modes/components/session-selector.d.ts +18 -9
  225. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  226. package/dist/types/modes/components/settings-selector.d.ts +17 -4
  227. package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
  228. package/dist/types/modes/components/status-line/component.d.ts +61 -0
  229. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  230. package/dist/types/modes/components/status-line/types.d.ts +47 -3
  231. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  232. package/dist/types/modes/components/tool-execution.d.ts +49 -2
  233. package/dist/types/modes/components/transcript-container.d.ts +76 -26
  234. package/dist/types/modes/components/tree-selector.d.ts +2 -2
  235. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  236. package/dist/types/modes/components/usage-row.d.ts +3 -0
  237. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  238. package/dist/types/modes/components/user-message.d.ts +2 -1
  239. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  240. package/dist/types/modes/components/welcome.d.ts +12 -2
  241. package/dist/types/modes/controllers/command-controller.d.ts +3 -2
  242. package/dist/types/modes/controllers/event-controller.d.ts +7 -1
  243. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  244. package/dist/types/modes/controllers/input-controller.d.ts +25 -3
  245. package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
  246. package/dist/types/modes/controllers/selector-controller.d.ts +5 -2
  247. package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
  248. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  249. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  250. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  251. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  252. package/dist/types/modes/image-references.d.ts +14 -3
  253. package/dist/types/modes/index.d.ts +8 -7
  254. package/dist/types/modes/interactive-mode.d.ts +92 -16
  255. package/dist/types/modes/magic-keywords.d.ts +14 -2
  256. package/dist/types/modes/markdown-prose.d.ts +1 -1
  257. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  258. package/dist/types/modes/rpc/rpc-client.d.ts +48 -2
  259. package/dist/types/modes/rpc/rpc-mode.d.ts +67 -2
  260. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  261. package/dist/types/modes/rpc/rpc-types.d.ts +113 -1
  262. package/dist/types/modes/runtime-init.d.ts +4 -0
  263. package/dist/types/modes/session-observer-registry.d.ts +9 -0
  264. package/dist/types/modes/setup-version.d.ts +11 -0
  265. package/dist/types/modes/setup-wizard/index.d.ts +7 -2
  266. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  267. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +4 -1
  268. package/dist/types/modes/setup-wizard/scenes/types.d.ts +11 -2
  269. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +6 -2
  270. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  271. package/dist/types/modes/theme/theme.d.ts +42 -7
  272. package/dist/types/modes/types.d.ts +62 -13
  273. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  274. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  275. package/dist/types/modes/utils/ui-helpers.d.ts +4 -4
  276. package/dist/types/modes/workflow.d.ts +3 -3
  277. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  278. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  279. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  280. package/dist/types/registry/agent-registry.d.ts +33 -5
  281. package/dist/types/sdk.d.ts +46 -4
  282. package/dist/types/secrets/index.d.ts +1 -1
  283. package/dist/types/secrets/obfuscator.d.ts +9 -3
  284. package/dist/types/session/agent-session.d.ts +136 -66
  285. package/dist/types/session/agent-storage.d.ts +2 -1
  286. package/dist/types/session/auth-broker-config.d.ts +4 -0
  287. package/dist/types/session/auth-storage.d.ts +1 -1
  288. package/dist/types/session/codex-auto-reset.d.ts +111 -0
  289. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  290. package/dist/types/session/messages.d.ts +26 -15
  291. package/dist/types/session/session-context.d.ts +39 -0
  292. package/dist/types/session/session-entries.d.ts +159 -0
  293. package/dist/types/session/session-history-format.d.ts +12 -0
  294. package/dist/types/session/session-listing.d.ts +69 -0
  295. package/dist/types/session/session-loader.d.ts +16 -0
  296. package/dist/types/session/session-manager.d.ts +107 -440
  297. package/dist/types/session/session-migrations.d.ts +12 -0
  298. package/dist/types/session/session-paths.d.ts +25 -0
  299. package/dist/types/session/session-persistence.d.ts +8 -0
  300. package/dist/types/session/session-storage.d.ts +11 -7
  301. package/dist/types/session/snapcompact-inline.d.ts +145 -0
  302. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  303. package/dist/types/session/streaming-output.d.ts +46 -0
  304. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  305. package/dist/types/session/yield-queue.d.ts +10 -1
  306. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  307. package/dist/types/slash-commands/available-commands.d.ts +34 -0
  308. package/dist/types/slash-commands/builtin-registry.d.ts +10 -0
  309. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  310. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  311. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  312. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  313. package/dist/types/slash-commands/types.d.ts +5 -9
  314. package/dist/types/ssh/connection-manager.d.ts +8 -0
  315. package/dist/types/stt/asr-client.d.ts +90 -0
  316. package/dist/types/stt/asr-protocol.d.ts +97 -0
  317. package/dist/types/stt/asr-worker.d.ts +2 -0
  318. package/dist/types/stt/downloader.d.ts +38 -0
  319. package/dist/types/stt/endpointer.d.ts +59 -0
  320. package/dist/types/stt/index.d.ts +5 -1
  321. package/dist/types/stt/models.d.ts +120 -0
  322. package/dist/types/stt/recorder.d.ts +17 -0
  323. package/dist/types/stt/stt-controller.d.ts +6 -0
  324. package/dist/types/stt/transcriber.d.ts +5 -7
  325. package/dist/types/stt/wav.d.ts +29 -0
  326. package/dist/types/system-prompt.d.ts +9 -1
  327. package/dist/types/task/commands.d.ts +1 -1
  328. package/dist/types/task/discovery.d.ts +1 -2
  329. package/dist/types/task/executor.d.ts +61 -2
  330. package/dist/types/task/index.d.ts +37 -6
  331. package/dist/types/task/output-manager.d.ts +0 -7
  332. package/dist/types/task/parallel.d.ts +2 -2
  333. package/dist/types/task/prometheus-command.d.ts +2 -2
  334. package/dist/types/task/render.d.ts +20 -7
  335. package/dist/types/task/repair-args.d.ts +8 -7
  336. package/dist/types/task/types.d.ts +109 -52
  337. package/dist/types/task/worktree.d.ts +2 -0
  338. package/dist/types/telemetry-export.d.ts +2 -2
  339. package/dist/types/thinking.d.ts +4 -0
  340. package/dist/types/tiny/models.d.ts +1 -1
  341. package/dist/types/tiny/title-client.d.ts +12 -1
  342. package/dist/types/tiny/title-protocol.d.ts +1 -0
  343. package/dist/types/tools/archive-reader.d.ts +5 -0
  344. package/dist/types/tools/ask.d.ts +6 -1
  345. package/dist/types/tools/ast-edit.d.ts +4 -1
  346. package/dist/types/tools/ast-grep.d.ts +4 -1
  347. package/dist/types/tools/bash.d.ts +5 -2
  348. package/dist/types/tools/browser/attach.d.ts +4 -4
  349. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  350. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  351. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  352. package/dist/types/tools/browser/registry.d.ts +17 -3
  353. package/dist/types/tools/browser/render.d.ts +2 -0
  354. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  355. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  356. package/dist/types/tools/browser/tab-worker.d.ts +18 -1
  357. package/dist/types/tools/browser.d.ts +3 -1
  358. package/dist/types/tools/checkpoint.d.ts +1 -1
  359. package/dist/types/tools/conflict-detect.d.ts +16 -0
  360. package/dist/types/tools/debug.d.ts +1 -1
  361. package/dist/types/tools/eval-render.d.ts +1 -8
  362. package/dist/types/tools/eval.d.ts +9 -1
  363. package/dist/types/tools/fetch.d.ts +17 -8
  364. package/dist/types/tools/find.d.ts +1 -8
  365. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  366. package/dist/types/tools/gh.d.ts +4 -1
  367. package/dist/types/tools/github-cache.d.ts +19 -0
  368. package/dist/types/tools/grouped-file-output.d.ts +46 -12
  369. package/dist/types/tools/image-gen.d.ts +1 -1
  370. package/dist/types/tools/index.d.ts +89 -8
  371. package/dist/types/tools/inspect-image.d.ts +1 -1
  372. package/dist/types/tools/irc.d.ts +79 -39
  373. package/dist/types/tools/job.d.ts +8 -2
  374. package/dist/types/tools/learn.d.ts +51 -0
  375. package/dist/types/tools/manage-skill.d.ts +40 -0
  376. package/dist/types/tools/memory-edit.d.ts +2 -2
  377. package/dist/types/tools/memory-recall.d.ts +1 -1
  378. package/dist/types/tools/memory-reflect.d.ts +1 -1
  379. package/dist/types/tools/memory-render.d.ts +4 -1
  380. package/dist/types/tools/memory-retain.d.ts +1 -1
  381. package/dist/types/tools/path-utils.d.ts +17 -5
  382. package/dist/types/tools/plan-mode-guard.d.ts +18 -9
  383. package/dist/types/tools/read.d.ts +3 -2
  384. package/dist/types/tools/render-mermaid.d.ts +1 -1
  385. package/dist/types/tools/render-utils.d.ts +47 -27
  386. package/dist/types/tools/renderers.d.ts +10 -2
  387. package/dist/types/tools/report-tool-issue.d.ts +6 -1
  388. package/dist/types/tools/resolve.d.ts +1 -1
  389. package/dist/types/tools/review.d.ts +1 -1
  390. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  391. package/dist/types/tools/search.d.ts +7 -3
  392. package/dist/types/tools/sqlite-reader.d.ts +4 -0
  393. package/dist/types/tools/ssh.d.ts +2 -1
  394. package/dist/types/tools/todo.d.ts +7 -15
  395. package/dist/types/tools/tool-result.d.ts +2 -0
  396. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  397. package/dist/types/tools/tts.d.ts +26 -1
  398. package/dist/types/tools/write.d.ts +6 -3
  399. package/dist/types/tools/yield.d.ts +8 -0
  400. package/dist/types/tts/downloader.d.ts +20 -0
  401. package/dist/types/tts/index.d.ts +8 -0
  402. package/dist/types/tts/models.d.ts +82 -0
  403. package/dist/types/tts/player.d.ts +32 -0
  404. package/dist/types/tts/runtime.d.ts +6 -0
  405. package/dist/types/tts/streaming-player.d.ts +41 -0
  406. package/dist/types/tts/tts-client.d.ts +93 -0
  407. package/dist/types/tts/tts-protocol.d.ts +95 -0
  408. package/dist/types/tts/tts-worker.d.ts +2 -0
  409. package/dist/types/tts/vocalizer.d.ts +41 -0
  410. package/dist/types/tts/wav.d.ts +8 -0
  411. package/dist/types/tui/code-cell.d.ts +0 -2
  412. package/dist/types/tui/hyperlink.d.ts +13 -7
  413. package/dist/types/tui/output-block.d.ts +16 -22
  414. package/dist/types/tui/status-line.d.ts +3 -0
  415. package/dist/types/utils/block-context.d.ts +35 -0
  416. package/dist/types/utils/changelog.d.ts +8 -0
  417. package/dist/types/utils/clipboard.d.ts +4 -3
  418. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  419. package/dist/types/utils/file-mentions.d.ts +7 -0
  420. package/dist/types/utils/git.d.ts +22 -3
  421. package/dist/types/utils/image-loading.d.ts +30 -1
  422. package/dist/types/utils/session-color.d.ts +15 -3
  423. package/dist/types/utils/thinking-display.d.ts +17 -0
  424. package/dist/types/utils/title-generator.d.ts +3 -2
  425. package/dist/types/utils/tool-choice.d.ts +8 -0
  426. package/dist/types/utils/tools-manager.d.ts +2 -1
  427. package/dist/types/web/kagi.d.ts +2 -2
  428. package/dist/types/web/parallel.d.ts +3 -0
  429. package/dist/types/web/scrapers/github.d.ts +22 -0
  430. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  431. package/dist/types/web/scrapers/types.d.ts +12 -0
  432. package/dist/types/web/search/index.d.ts +1 -1
  433. package/dist/types/web/search/providers/anthropic.d.ts +2 -1
  434. package/dist/types/web/search/providers/base.d.ts +2 -1
  435. package/dist/types/web/search/providers/brave.d.ts +2 -1
  436. package/dist/types/web/search/providers/codex.d.ts +2 -1
  437. package/dist/types/web/search/providers/exa.d.ts +2 -1
  438. package/dist/types/web/search/providers/gemini.d.ts +10 -6
  439. package/dist/types/web/search/providers/jina.d.ts +7 -2
  440. package/dist/types/web/search/providers/kagi.d.ts +7 -2
  441. package/dist/types/web/search/providers/kimi.d.ts +7 -2
  442. package/dist/types/web/search/providers/parallel.d.ts +2 -1
  443. package/dist/types/web/search/providers/perplexity.d.ts +10 -2
  444. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  445. package/dist/types/web/search/providers/synthetic.d.ts +7 -3
  446. package/dist/types/web/search/providers/tavily.d.ts +2 -1
  447. package/dist/types/web/search/providers/zai.d.ts +2 -1
  448. package/dist/types/web/search/types.d.ts +1 -1
  449. package/examples/extensions/api-demo.ts +2 -2
  450. package/package.json +41 -15
  451. package/scripts/bench-guard.ts +71 -0
  452. package/scripts/build-binary.ts +24 -25
  453. package/scripts/bundle-dist.ts +97 -0
  454. package/scripts/generate-share-viewer.ts +34 -0
  455. package/scripts/prometheus +42 -0
  456. package/scripts/prometheus.ts +20 -0
  457. package/src/async/index.ts +0 -1
  458. package/src/async/job-manager.ts +106 -3
  459. package/src/auto-thinking/classifier.ts +2 -1
  460. package/src/autolearn/controller.ts +139 -0
  461. package/src/autolearn/managed-skills.ts +257 -0
  462. package/src/autoresearch/dashboard.ts +1 -1
  463. package/src/autoresearch/prompt-setup.md +6 -6
  464. package/src/autoresearch/prompt.md +6 -6
  465. package/src/autoresearch/state.ts +1 -1
  466. package/src/autoresearch/storage.ts +2 -1
  467. package/src/autoresearch/tools/init-experiment.ts +1 -1
  468. package/src/autoresearch/tools/log-experiment.ts +1 -1
  469. package/src/autoresearch/tools/run-experiment.ts +1 -1
  470. package/src/autoresearch/tools/update-notes.ts +1 -1
  471. package/src/autoresearch/types.ts +1 -1
  472. package/src/capability/context-file.ts +0 -14
  473. package/src/capability/fs.ts +10 -0
  474. package/src/capability/index.ts +1 -6
  475. package/src/capability/mcp.ts +1 -0
  476. package/src/capability/rule-buckets.ts +4 -2
  477. package/src/capability/rule.ts +10 -1
  478. package/src/capability/types.ts +0 -4
  479. package/src/cli/args.ts +66 -13
  480. package/src/cli/auth-broker-cli.ts +6 -7
  481. package/src/cli/auth-gateway-cli.ts +8 -9
  482. package/src/cli/bench-cli.ts +437 -0
  483. package/src/cli/claude-trace-cli.ts +28 -50
  484. package/src/cli/completion-gen.ts +28 -28
  485. package/src/cli/dry-balance-cli.ts +56 -23
  486. package/src/cli/gallery-cli.ts +231 -0
  487. package/src/cli/gallery-fixtures/agentic.ts +407 -0
  488. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  489. package/src/cli/gallery-fixtures/edit.ts +194 -0
  490. package/src/cli/gallery-fixtures/fs.ts +220 -0
  491. package/src/cli/gallery-fixtures/index.ts +40 -0
  492. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  493. package/src/cli/gallery-fixtures/memory.ts +81 -0
  494. package/src/cli/gallery-fixtures/misc.ts +250 -0
  495. package/src/cli/gallery-fixtures/search.ts +213 -0
  496. package/src/cli/gallery-fixtures/shell.ts +167 -0
  497. package/src/cli/gallery-fixtures/types.ts +57 -0
  498. package/src/cli/gallery-fixtures/web.ts +158 -0
  499. package/src/cli/gallery-screenshot.ts +279 -0
  500. package/src/cli/grievances-cli.ts +1 -1
  501. package/src/cli/list-models.ts +16 -174
  502. package/src/cli/models-cli.ts +429 -0
  503. package/src/cli/session-picker.ts +2 -1
  504. package/src/cli/setup-cli.ts +148 -47
  505. package/src/cli/setup-model-picker.ts +43 -0
  506. package/src/cli/startup-cwd.ts +68 -0
  507. package/src/cli/update-cli.ts +144 -272
  508. package/src/cli/usage-cli.ts +774 -0
  509. package/src/cli-commands.ts +36 -0
  510. package/src/cli.ts +141 -32
  511. package/src/collab/crypto.ts +63 -0
  512. package/src/collab/guest.ts +451 -0
  513. package/src/collab/host.ts +565 -0
  514. package/src/collab/protocol.ts +241 -0
  515. package/src/collab/relay-client.ts +216 -0
  516. package/src/commands/bench.ts +42 -0
  517. package/src/commands/complete.ts +1 -1
  518. package/src/commands/gallery.ts +52 -0
  519. package/src/commands/install.ts +1 -1
  520. package/src/commands/join.ts +39 -0
  521. package/src/commands/launch.ts +8 -4
  522. package/src/commands/models.ts +61 -0
  523. package/src/commands/read.ts +6 -3
  524. package/src/commands/say.ts +102 -0
  525. package/src/commands/setup.ts +1 -1
  526. package/src/commands/token.ts +89 -0
  527. package/src/commands/usage.ts +43 -0
  528. package/src/commit/agentic/agent.ts +2 -1
  529. package/src/commit/agentic/tools/analyze-file.ts +42 -20
  530. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  531. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  532. package/src/commit/agentic/tools/git-overview.ts +1 -1
  533. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  534. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  535. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  536. package/src/commit/agentic/tools/schemas.ts +1 -1
  537. package/src/commit/agentic/tools/split-commit.ts +9 -2
  538. package/src/commit/analysis/conventional.ts +2 -2
  539. package/src/commit/analysis/summary.ts +3 -3
  540. package/src/commit/changelog/generate.ts +3 -3
  541. package/src/commit/changelog/index.ts +2 -2
  542. package/src/commit/map-reduce/index.ts +3 -3
  543. package/src/commit/map-reduce/map-phase.ts +2 -2
  544. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  545. package/src/commit/model-selection.ts +35 -12
  546. package/src/commit/pipeline.ts +4 -4
  547. package/src/commit/shared-llm.ts +1 -1
  548. package/src/config/api-key-resolver.ts +67 -0
  549. package/src/config/append-only-context-mode.ts +6 -12
  550. package/src/config/keybindings.ts +9 -4
  551. package/src/config/mcp-schema.json +4 -0
  552. package/src/config/model-discovery.ts +574 -0
  553. package/src/config/model-equivalence.ts +5 -4
  554. package/src/config/model-registry.ts +659 -1093
  555. package/src/config/model-resolver.ts +374 -174
  556. package/src/config/model-roles.ts +88 -0
  557. package/src/config/models-config-schema.ts +61 -9
  558. package/src/config/models-config.ts +130 -0
  559. package/src/config/settings-schema.ts +1441 -387
  560. package/src/config/settings.ts +261 -69
  561. package/src/dap/client.ts +138 -53
  562. package/src/dap/config.ts +41 -2
  563. package/src/dap/defaults.json +1 -0
  564. package/src/dap/session.ts +263 -161
  565. package/src/dap/types.ts +10 -0
  566. package/src/debug/index.ts +50 -60
  567. package/src/debug/log-viewer.ts +1 -1
  568. package/src/debug/protocol-probe.ts +1 -1
  569. package/src/debug/raw-sse-buffer.ts +7 -4
  570. package/src/debug/raw-sse.ts +1 -1
  571. package/src/debug/report-bundle.ts +9 -0
  572. package/src/debug/terminal-info.ts +0 -3
  573. package/src/discovery/agents-md.ts +25 -21
  574. package/src/discovery/agents.ts +9 -15
  575. package/src/discovery/at-imports.ts +273 -0
  576. package/src/discovery/builtin-rules/index.ts +4 -0
  577. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  578. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  579. package/src/discovery/builtin.ts +45 -23
  580. package/src/discovery/claude-plugins.ts +44 -5
  581. package/src/discovery/helpers.ts +50 -9
  582. package/src/discovery/prometheus-extension-roots.ts +10 -10
  583. package/src/discovery/prometheus-plugins.ts +10 -10
  584. package/src/edit/diff.ts +191 -4
  585. package/src/edit/file-snapshot-store.ts +34 -1
  586. package/src/edit/hashline/block-resolver.ts +20 -1
  587. package/src/edit/hashline/diff.ts +123 -2
  588. package/src/edit/hashline/execute.ts +60 -4
  589. package/src/edit/hashline/filesystem.ts +2 -1
  590. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  591. package/src/edit/hashline/params.ts +1 -1
  592. package/src/edit/index.ts +47 -18
  593. package/src/edit/modes/apply-patch.ts +1 -1
  594. package/src/edit/modes/patch.ts +59 -3
  595. package/src/edit/modes/replace.ts +58 -24
  596. package/src/edit/notebook.ts +22 -2
  597. package/src/edit/renderer.ts +315 -151
  598. package/src/eval/__tests__/agent-bridge.test.ts +105 -39
  599. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  600. package/src/eval/__tests__/completion-bridge.test.ts +412 -0
  601. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  602. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  603. package/src/eval/__tests__/llm-bridge.test.ts +6 -4
  604. package/src/eval/__tests__/shared-executors.test.ts +34 -92
  605. package/src/eval/agent-bridge.ts +39 -23
  606. package/src/eval/backend.ts +15 -2
  607. package/src/eval/bridge-timeout.ts +1 -1
  608. package/src/eval/completion-bridge.ts +203 -0
  609. package/src/eval/idle-timeout.ts +3 -10
  610. package/src/eval/js/context-manager.ts +108 -31
  611. package/src/eval/js/executor.ts +9 -2
  612. package/src/eval/js/index.ts +7 -3
  613. package/src/eval/js/shared/helpers.ts +59 -13
  614. package/src/eval/js/shared/local-module-loader.ts +2 -2
  615. package/src/eval/js/shared/prelude.txt +167 -30
  616. package/src/eval/js/shared/rewrite-imports.ts +58 -34
  617. package/src/eval/js/shared/runtime.ts +24 -16
  618. package/src/eval/js/tool-bridge.ts +4 -0
  619. package/src/eval/js/worker-core.ts +1 -0
  620. package/src/eval/js/worker-entry.ts +6 -0
  621. package/src/eval/js/worker-protocol.ts +6 -0
  622. package/src/eval/llm-bridge.ts +2 -1
  623. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  624. package/src/eval/py/executor.ts +70 -26
  625. package/src/eval/py/index.ts +13 -4
  626. package/src/eval/py/kernel.ts +48 -9
  627. package/src/eval/py/prelude.py +73 -24
  628. package/src/eval/py/runner.py +133 -28
  629. package/src/eval/py/runtime.ts +38 -1
  630. package/src/exa/index.ts +1 -26
  631. package/src/exa/mcp-client.ts +10 -10
  632. package/src/exa/types.ts +0 -97
  633. package/src/exec/bash-executor.ts +104 -7
  634. package/src/export/custom-share.ts +1 -1
  635. package/src/export/html/index.ts +119 -17
  636. package/src/export/html/share-loader.js +102 -0
  637. package/src/export/html/template-js.ts +6 -0
  638. package/src/export/html/template.css +745 -459
  639. package/src/export/html/template.css.d.ts +2 -0
  640. package/src/export/html/template.html +6 -3
  641. package/src/export/html/template.js +277 -891
  642. package/src/export/html/tool-views.generated.d.ts +2 -0
  643. package/src/export/html/tool-views.generated.js +38 -0
  644. package/src/export/share.ts +269 -0
  645. package/src/export/ttsr.ts +122 -1
  646. package/src/extensibility/custom-commands/loader.ts +7 -4
  647. package/src/extensibility/custom-commands/types.ts +9 -4
  648. package/src/extensibility/custom-tools/loader.ts +51 -23
  649. package/src/extensibility/custom-tools/types.ts +16 -8
  650. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  651. package/src/extensibility/extensions/index.ts +1 -0
  652. package/src/extensibility/extensions/loader.ts +70 -20
  653. package/src/extensibility/extensions/model-api.ts +41 -0
  654. package/src/extensibility/extensions/runner.ts +12 -2
  655. package/src/extensibility/extensions/types.ts +83 -11
  656. package/src/extensibility/extensions/wrapper.ts +41 -5
  657. package/src/extensibility/hooks/index.ts +2 -1
  658. package/src/extensibility/hooks/loader.ts +6 -3
  659. package/src/extensibility/hooks/types.ts +11 -5
  660. package/src/extensibility/{legacy-pi-ai-shim.ts → legacy-package-ai-shim.ts} +2 -2
  661. package/src/extensibility/plugins/doctor.ts +1 -2
  662. package/src/extensibility/plugins/installer.ts +2 -2
  663. package/src/extensibility/plugins/{legacy-pi-compat.ts → legacy-package-compat.ts} +165 -77
  664. package/src/extensibility/plugins/loader.ts +34 -23
  665. package/src/extensibility/plugins/manager.ts +226 -95
  666. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  667. package/src/extensibility/plugins/types.ts +3 -3
  668. package/src/extensibility/shared-events.ts +3 -3
  669. package/src/extensibility/skills.ts +113 -9
  670. package/src/extensibility/slash-commands.ts +1 -97
  671. package/src/goals/guided-setup.ts +133 -0
  672. package/src/goals/state.ts +1 -1
  673. package/src/goals/tools/goal-tool.ts +38 -28
  674. package/src/hindsight/bank.ts +17 -2
  675. package/src/hindsight/client.ts +27 -2
  676. package/src/hindsight/mental-models.ts +59 -12
  677. package/src/hindsight/state.ts +12 -3
  678. package/src/hindsight/transcript.ts +1 -1
  679. package/src/index.ts +5 -0
  680. package/src/internal-urls/artifact-protocol.ts +11 -2
  681. package/src/internal-urls/docs-index.generated.ts +9 -7
  682. package/src/internal-urls/history-protocol.ts +113 -0
  683. package/src/internal-urls/index.ts +1 -0
  684. package/src/internal-urls/issue-pr-protocol.ts +22 -9
  685. package/src/internal-urls/local-protocol.ts +42 -7
  686. package/src/internal-urls/memory-protocol.ts +4 -31
  687. package/src/internal-urls/router.ts +3 -1
  688. package/src/internal-urls/types.ts +1 -1
  689. package/src/irc/bus.ts +303 -0
  690. package/src/lib/xai-http.ts +3 -3
  691. package/src/lsp/client.ts +245 -104
  692. package/src/lsp/clients/biome-client.ts +101 -39
  693. package/src/lsp/clients/lsp-linter-client.ts +2 -10
  694. package/src/lsp/config.ts +15 -5
  695. package/src/lsp/defaults.json +6 -0
  696. package/src/lsp/edits.ts +143 -95
  697. package/src/lsp/format-options.ts +119 -0
  698. package/src/lsp/index.ts +233 -93
  699. package/src/lsp/render.ts +11 -35
  700. package/src/lsp/types.ts +13 -1
  701. package/src/lsp/utils.ts +31 -12
  702. package/src/main.ts +396 -216
  703. package/src/mcp/config-writer.ts +7 -3
  704. package/src/mcp/json-rpc.ts +35 -5
  705. package/src/mcp/manager.ts +31 -16
  706. package/src/mcp/oauth-discovery.ts +34 -4
  707. package/src/mcp/oauth-flow.ts +61 -8
  708. package/src/mcp/render.ts +7 -1
  709. package/src/mcp/startup-events.ts +21 -0
  710. package/src/mcp/tool-bridge.ts +2 -0
  711. package/src/mcp/transports/stdio.ts +224 -4
  712. package/src/mcp/types.ts +2 -0
  713. package/src/memories/index.ts +174 -1128
  714. package/src/memories/storage.ts +2 -41
  715. package/src/memory-backend/index.ts +14 -1
  716. package/src/memory-backend/local-backend.ts +18 -3
  717. package/src/memory-backend/off-backend.ts +9 -0
  718. package/src/memory-backend/resolve.ts +4 -6
  719. package/src/memory-backend/runtime.ts +66 -0
  720. package/src/memory-backend/types.ts +82 -2
  721. package/src/mnemopi/backend.ts +220 -28
  722. package/src/mnemopi/config.ts +138 -33
  723. package/src/mnemopi/state.ts +91 -11
  724. package/src/modes/acp/acp-agent.ts +149 -142
  725. package/src/modes/acp/acp-event-mapper.ts +5 -1
  726. package/src/modes/components/agent-dashboard.ts +17 -11
  727. package/src/modes/components/agent-hub.ts +1346 -0
  728. package/src/modes/components/assistant-message.ts +190 -80
  729. package/src/modes/components/bash-execution.ts +1 -1
  730. package/src/modes/components/btw-panel.ts +5 -1
  731. package/src/modes/components/chat-block.ts +111 -0
  732. package/src/modes/components/collab-prompt-message.ts +30 -0
  733. package/src/modes/components/compaction-summary-message.ts +168 -33
  734. package/src/modes/components/copy-selector.ts +2 -45
  735. package/src/modes/components/custom-editor.test.ts +96 -0
  736. package/src/modes/components/custom-editor.ts +405 -118
  737. package/src/modes/components/custom-message.ts +1 -3
  738. package/src/modes/components/diff.ts +13 -2
  739. package/src/modes/components/dynamic-border.ts +12 -3
  740. package/src/modes/components/execution-shared.ts +1 -2
  741. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  742. package/src/modes/components/extensions/extension-list.ts +1 -1
  743. package/src/modes/components/extensions/inspector-panel.ts +7 -3
  744. package/src/modes/components/extensions/state-manager.ts +36 -41
  745. package/src/modes/components/footer.ts +4 -2
  746. package/src/modes/components/history-search.ts +1 -1
  747. package/src/modes/components/hook-editor.ts +8 -0
  748. package/src/modes/components/hook-input.ts +8 -0
  749. package/src/modes/components/hook-message.ts +1 -3
  750. package/src/modes/components/hook-selector.ts +6 -7
  751. package/src/modes/components/index.ts +1 -0
  752. package/src/modes/components/late-diagnostics-message.ts +60 -0
  753. package/src/modes/components/login-dialog.ts +1 -1
  754. package/src/modes/components/logout-account-selector.ts +130 -0
  755. package/src/modes/components/mcp-add-wizard.ts +14 -1
  756. package/src/modes/components/model-selector.ts +177 -75
  757. package/src/modes/components/oauth-selector.ts +102 -16
  758. package/src/modes/components/overlay-box.ts +108 -0
  759. package/src/modes/components/plan-review-overlay.ts +845 -0
  760. package/src/modes/components/plan-toc.ts +138 -0
  761. package/src/modes/components/plugin-settings.ts +22 -5
  762. package/src/modes/components/read-tool-group.ts +442 -39
  763. package/src/modes/components/reset-usage-selector.ts +161 -0
  764. package/src/modes/components/segment-track.ts +44 -7
  765. package/src/modes/components/session-selector.ts +97 -37
  766. package/src/modes/components/settings-defs.ts +28 -6
  767. package/src/modes/components/settings-selector.ts +541 -93
  768. package/src/modes/components/skill-message.ts +0 -1
  769. package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
  770. package/src/modes/components/snapcompact-shape-preview.ts +193 -0
  771. package/src/modes/components/{status-line.ts → status-line/component.ts} +205 -168
  772. package/src/modes/components/status-line/index.ts +1 -0
  773. package/src/modes/components/status-line/presets.ts +3 -3
  774. package/src/modes/components/status-line/segments.ts +26 -7
  775. package/src/modes/components/status-line/types.ts +40 -9
  776. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  777. package/src/modes/components/tips.txt +7 -3
  778. package/src/modes/components/todo-reminder.ts +0 -2
  779. package/src/modes/components/tool-execution.ts +236 -103
  780. package/src/modes/components/transcript-container.ts +724 -99
  781. package/src/modes/components/tree-selector.ts +19 -4
  782. package/src/modes/components/ttsr-notification.ts +72 -30
  783. package/src/modes/components/usage-row.ts +18 -0
  784. package/src/modes/components/user-message-selector.ts +1 -1
  785. package/src/modes/components/user-message.ts +28 -12
  786. package/src/modes/components/visual-truncate.ts +1 -1
  787. package/src/modes/components/welcome.ts +80 -22
  788. package/src/modes/controllers/command-controller-shared.ts +7 -6
  789. package/src/modes/controllers/command-controller.ts +210 -180
  790. package/src/modes/controllers/event-controller.ts +352 -142
  791. package/src/modes/controllers/extension-ui-controller.ts +167 -208
  792. package/src/modes/controllers/input-controller.ts +778 -162
  793. package/src/modes/controllers/mcp-command-controller.ts +232 -80
  794. package/src/modes/controllers/selector-controller.ts +284 -145
  795. package/src/modes/controllers/session-focus-controller.ts +112 -0
  796. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  797. package/src/modes/controllers/streaming-reveal.ts +295 -0
  798. package/src/modes/controllers/tan-command-controller.ts +173 -0
  799. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  800. package/src/modes/gradient-highlight.ts +21 -9
  801. package/src/modes/image-references.ts +33 -7
  802. package/src/modes/index.ts +8 -25
  803. package/src/modes/interactive-mode.ts +840 -186
  804. package/src/modes/magic-keywords.ts +28 -6
  805. package/src/modes/markdown-prose.ts +1 -1
  806. package/src/modes/oauth-manual-input.ts +30 -3
  807. package/src/modes/rpc/rpc-client.ts +186 -3
  808. package/src/modes/rpc/rpc-mode.ts +318 -24
  809. package/src/modes/rpc/rpc-subagents.ts +265 -0
  810. package/src/modes/rpc/rpc-types.ts +111 -2
  811. package/src/modes/runtime-init.ts +28 -3
  812. package/src/modes/session-observer-registry.ts +72 -3
  813. package/src/modes/setup-version.ts +11 -0
  814. package/src/modes/setup-wizard/index.ts +16 -4
  815. package/src/modes/setup-wizard/lazy.ts +16 -0
  816. package/src/modes/setup-wizard/scenes/glyph.ts +25 -7
  817. package/src/modes/setup-wizard/scenes/providers.ts +45 -12
  818. package/src/modes/setup-wizard/scenes/sign-in.ts +14 -13
  819. package/src/modes/setup-wizard/scenes/splash.ts +1 -1
  820. package/src/modes/setup-wizard/scenes/theme.ts +29 -2
  821. package/src/modes/setup-wizard/scenes/types.ts +11 -2
  822. package/src/modes/setup-wizard/scenes/web-search.ts +26 -9
  823. package/src/modes/setup-wizard/wizard-overlay.ts +40 -3
  824. package/src/modes/shared.ts +2 -0
  825. package/src/modes/theme/defaults/dark-poimandres.json +1 -1
  826. package/src/modes/theme/defaults/light-poimandres.json +1 -1
  827. package/src/modes/theme/shimmer.ts +20 -9
  828. package/src/modes/theme/theme-schema.json +1 -1
  829. package/src/modes/theme/theme.ts +342 -82
  830. package/src/modes/types.ts +60 -18
  831. package/src/modes/utils/context-usage.ts +88 -8
  832. package/src/modes/utils/copy-targets.ts +133 -27
  833. package/src/modes/utils/hotkeys-markdown.ts +3 -2
  834. package/src/modes/utils/ui-helpers.ts +191 -110
  835. package/src/modes/workflow.ts +10 -10
  836. package/src/plan-mode/approved-plan.ts +66 -43
  837. package/src/plan-mode/plan-protection.ts +4 -4
  838. package/src/priority.json +5 -1
  839. package/src/prompts/agents/designer.md +1 -1
  840. package/src/prompts/agents/explore.md +3 -3
  841. package/src/prompts/agents/librarian.md +2 -3
  842. package/src/prompts/agents/oracle.md +2 -2
  843. package/src/prompts/agents/plan.md +6 -6
  844. package/src/prompts/agents/reviewer.md +1 -1
  845. package/src/prompts/agents/task.md +6 -5
  846. package/src/prompts/bench.md +12 -0
  847. package/src/prompts/ci-green-request.md +5 -7
  848. package/src/prompts/goals/goal-budget-limit.md +2 -2
  849. package/src/prompts/goals/goal-continuation.md +4 -4
  850. package/src/prompts/goals/goal-mode-active.md +1 -1
  851. package/src/prompts/goals/guided-goal-interview.md +8 -0
  852. package/src/prompts/goals/guided-goal-system.md +12 -0
  853. package/src/prompts/memories/consolidation.md +2 -7
  854. package/src/prompts/memories/consolidation_system.md +4 -0
  855. package/src/prompts/memories/identity_review.md +2 -2
  856. package/src/prompts/memories/read-path.md +11 -10
  857. package/src/prompts/memories/stage_one_system.md +2 -2
  858. package/src/prompts/review-custom-request.md +1 -1
  859. package/src/prompts/system/agent-creation-architect.md +2 -2
  860. package/src/prompts/system/auto-continue.md +1 -1
  861. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  862. package/src/prompts/system/autolearn-guidance.md +7 -0
  863. package/src/prompts/system/autolearn-nudge.md +3 -0
  864. package/src/prompts/system/background-tan-dispatch.md +8 -0
  865. package/src/prompts/system/btw-user.md +2 -2
  866. package/src/prompts/system/commit-message-system.md +13 -1
  867. package/src/prompts/system/custom-system-prompt.md +1 -1
  868. package/src/prompts/system/eager-task.md +7 -0
  869. package/src/prompts/system/eager-todo.md +11 -6
  870. package/src/prompts/system/empty-stop-retry.md +4 -6
  871. package/src/prompts/system/irc-autoreply.md +6 -0
  872. package/src/prompts/system/irc-incoming.md +3 -4
  873. package/src/prompts/system/manual-continue.md +7 -0
  874. package/src/prompts/system/omfg-user.md +3 -4
  875. package/src/prompts/system/orchestrate-notice.md +10 -10
  876. package/src/prompts/system/personalities/default.md +26 -0
  877. package/src/prompts/system/personalities/friendly.md +17 -0
  878. package/src/prompts/system/personalities/pragmatic.md +15 -0
  879. package/src/prompts/system/plan-mode-active.md +70 -77
  880. package/src/prompts/system/plan-mode-approved.md +1 -1
  881. package/src/prompts/system/plan-mode-subagent.md +4 -5
  882. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  883. package/src/prompts/system/project-prompt.md +2 -2
  884. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  885. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  886. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  887. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  888. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  889. package/src/prompts/system/subagent-system-prompt.md +7 -8
  890. package/src/prompts/system/system-prompt.md +28 -57
  891. package/src/prompts/system/tiny-title-system.md +1 -1
  892. package/src/prompts/system/title-marker-instruction.md +1 -0
  893. package/src/prompts/system/title-system-marker.md +16 -0
  894. package/src/prompts/system/title-system.md +16 -3
  895. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  896. package/src/prompts/system/workflow-notice.md +4 -4
  897. package/src/prompts/tools/ast-edit.md +1 -1
  898. package/src/prompts/tools/ast-grep.md +2 -2
  899. package/src/prompts/tools/bash.md +16 -8
  900. package/src/prompts/tools/browser.md +33 -43
  901. package/src/prompts/tools/debug.md +1 -1
  902. package/src/prompts/tools/eval.md +31 -51
  903. package/src/prompts/tools/find.md +0 -1
  904. package/src/prompts/tools/github.md +8 -7
  905. package/src/prompts/tools/goal.md +1 -1
  906. package/src/prompts/tools/image-gen.md +1 -1
  907. package/src/prompts/tools/inspect-image-system.md +1 -1
  908. package/src/prompts/tools/irc.md +39 -31
  909. package/src/prompts/tools/job.md +2 -1
  910. package/src/prompts/tools/learn.md +7 -0
  911. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  912. package/src/prompts/tools/lsp.md +2 -2
  913. package/src/prompts/tools/manage-skill.md +9 -0
  914. package/src/prompts/tools/memory-edit.md +1 -1
  915. package/src/prompts/tools/patch.md +2 -2
  916. package/src/prompts/tools/read.md +31 -39
  917. package/src/prompts/tools/recall.md +1 -1
  918. package/src/prompts/tools/reflect.md +1 -1
  919. package/src/prompts/tools/render-mermaid.md +2 -2
  920. package/src/prompts/tools/replace.md +4 -10
  921. package/src/prompts/tools/rewind.md +2 -2
  922. package/src/prompts/tools/search-tool-bm25.md +1 -9
  923. package/src/prompts/tools/search.md +0 -1
  924. package/src/prompts/tools/ssh.md +0 -4
  925. package/src/prompts/tools/task-summary.md +5 -16
  926. package/src/prompts/tools/task.md +47 -31
  927. package/src/prompts/tools/todo.md +6 -3
  928. package/src/registry/agent-lifecycle.ts +218 -0
  929. package/src/registry/agent-registry.ts +46 -5
  930. package/src/sdk.ts +692 -219
  931. package/src/secrets/index.ts +8 -1
  932. package/src/secrets/obfuscator.ts +40 -19
  933. package/src/session/agent-session.ts +1577 -806
  934. package/src/session/agent-storage.ts +18 -9
  935. package/src/session/auth-broker-config.ts +30 -1
  936. package/src/session/auth-storage.ts +6 -0
  937. package/src/session/codex-auto-reset.ts +202 -0
  938. package/src/session/history-storage.ts +3 -2
  939. package/src/session/indexed-session-storage.ts +7 -10
  940. package/src/session/messages.ts +59 -95
  941. package/src/session/session-context.ts +352 -0
  942. package/src/session/session-dump-format.ts +12 -3
  943. package/src/session/session-entries.ts +194 -0
  944. package/src/session/session-history-format.ts +246 -0
  945. package/src/session/session-listing.ts +588 -0
  946. package/src/session/session-loader.ts +106 -0
  947. package/src/session/session-manager.ts +1003 -2920
  948. package/src/session/session-migrations.ts +78 -0
  949. package/src/session/session-paths.ts +193 -0
  950. package/src/session/session-persistence.ts +131 -0
  951. package/src/session/session-storage.ts +91 -30
  952. package/src/session/snapcompact-inline.ts +542 -0
  953. package/src/session/snapcompact-savings-journal.ts +113 -0
  954. package/src/session/streaming-output.ts +248 -11
  955. package/src/session/tool-choice-queue.ts +23 -11
  956. package/src/session/yield-queue.ts +20 -2
  957. package/src/slash-commands/acp-builtins.ts +25 -1
  958. package/src/slash-commands/available-commands.ts +105 -0
  959. package/src/slash-commands/builtin-registry.ts +575 -49
  960. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  961. package/src/slash-commands/helpers/context-report.ts +28 -1
  962. package/src/slash-commands/helpers/logout.ts +88 -0
  963. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  964. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  965. package/src/slash-commands/helpers/usage-report.ts +38 -3
  966. package/src/slash-commands/types.ts +5 -9
  967. package/src/ssh/connection-manager.ts +27 -0
  968. package/src/ssh/ssh-executor.ts +60 -4
  969. package/src/stt/asr-client.ts +520 -0
  970. package/src/stt/asr-protocol.ts +65 -0
  971. package/src/stt/asr-worker.ts +790 -0
  972. package/src/stt/downloader.ts +107 -47
  973. package/src/stt/endpointer.ts +259 -0
  974. package/src/stt/index.ts +5 -1
  975. package/src/stt/models.ts +150 -0
  976. package/src/stt/recorder.ts +254 -67
  977. package/src/stt/stt-controller.ts +201 -22
  978. package/src/stt/transcriber.ts +37 -68
  979. package/src/stt/wav.ts +173 -0
  980. package/src/system-prompt.ts +52 -10
  981. package/src/task/agents.ts +3 -4
  982. package/src/task/commands.ts +3 -2
  983. package/src/task/discovery.ts +17 -24
  984. package/src/task/executor.ts +1054 -529
  985. package/src/task/index.ts +862 -757
  986. package/src/task/output-manager.ts +0 -11
  987. package/src/task/parallel.ts +3 -3
  988. package/src/task/prometheus-command.ts +2 -2
  989. package/src/task/render.ts +529 -182
  990. package/src/task/repair-args.ts +21 -9
  991. package/src/task/types.ts +144 -66
  992. package/src/task/worktree.ts +64 -56
  993. package/src/telemetry-export.ts +27 -9
  994. package/src/thinking.ts +9 -7
  995. package/src/tiny/models.ts +2 -2
  996. package/src/tiny/text.ts +5 -1
  997. package/src/tiny/title-client.ts +72 -20
  998. package/src/tiny/title-protocol.ts +1 -1
  999. package/src/tiny/worker.ts +23 -99
  1000. package/src/tool-discovery/tool-index.ts +2 -0
  1001. package/src/tools/archive-reader.ts +94 -2
  1002. package/src/tools/ask.ts +234 -177
  1003. package/src/tools/ast-edit.ts +136 -80
  1004. package/src/tools/ast-grep.ts +41 -45
  1005. package/src/tools/auto-generated-guard.ts +20 -3
  1006. package/src/tools/bash-interactive.ts +28 -8
  1007. package/src/tools/bash.ts +198 -35
  1008. package/src/tools/browser/attach.ts +26 -7
  1009. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  1010. package/src/tools/browser/cmux/rpc.ts +156 -0
  1011. package/src/tools/browser/cmux/socket-client.ts +309 -0
  1012. package/src/tools/browser/launch.ts +11 -2
  1013. package/src/tools/browser/readable.ts +19 -2
  1014. package/src/tools/browser/registry.ts +52 -5
  1015. package/src/tools/browser/render.ts +13 -5
  1016. package/src/tools/browser/tab-protocol.ts +2 -0
  1017. package/src/tools/browser/tab-supervisor.ts +256 -34
  1018. package/src/tools/browser/tab-worker.ts +259 -91
  1019. package/src/tools/browser.ts +44 -2
  1020. package/src/tools/checkpoint.ts +1 -1
  1021. package/src/tools/conflict-detect.ts +50 -4
  1022. package/src/tools/debug.ts +27 -12
  1023. package/src/tools/eval-render.ts +32 -35
  1024. package/src/tools/eval.ts +26 -12
  1025. package/src/tools/fetch.ts +450 -99
  1026. package/src/tools/find.ts +182 -142
  1027. package/src/tools/gh-cache-invalidation.ts +255 -0
  1028. package/src/tools/gh-renderer.ts +104 -51
  1029. package/src/tools/gh.ts +232 -37
  1030. package/src/tools/github-cache.ts +97 -7
  1031. package/src/tools/grouped-file-output.ts +159 -52
  1032. package/src/tools/image-gen.ts +237 -132
  1033. package/src/tools/index.ts +147 -26
  1034. package/src/tools/inspect-image-renderer.ts +74 -45
  1035. package/src/tools/inspect-image.ts +12 -6
  1036. package/src/tools/irc.ts +626 -173
  1037. package/src/tools/job.ts +106 -29
  1038. package/src/tools/learn.ts +144 -0
  1039. package/src/tools/manage-skill.ts +104 -0
  1040. package/src/tools/memory-edit.ts +4 -4
  1041. package/src/tools/memory-recall.ts +7 -9
  1042. package/src/tools/memory-reflect.ts +5 -9
  1043. package/src/tools/memory-render.ts +23 -6
  1044. package/src/tools/memory-retain.ts +4 -4
  1045. package/src/tools/path-utils.ts +102 -48
  1046. package/src/tools/plan-mode-guard.ts +101 -40
  1047. package/src/tools/read.ts +475 -120
  1048. package/src/tools/render-mermaid.ts +1 -1
  1049. package/src/tools/render-utils.ts +132 -76
  1050. package/src/tools/renderers.ts +12 -1
  1051. package/src/tools/report-tool-issue.ts +14 -6
  1052. package/src/tools/resolve.ts +20 -3
  1053. package/src/tools/review.ts +2 -2
  1054. package/src/tools/search-tool-bm25.ts +37 -24
  1055. package/src/tools/search.ts +233 -115
  1056. package/src/tools/sqlite-reader.ts +26 -17
  1057. package/src/tools/ssh.ts +20 -14
  1058. package/src/tools/todo.ts +197 -191
  1059. package/src/tools/tool-result.ts +8 -0
  1060. package/src/tools/tool-timeouts.ts +1 -1
  1061. package/src/tools/tts.ts +205 -74
  1062. package/src/tools/write.ts +291 -155
  1063. package/src/tools/yield.ts +10 -1
  1064. package/src/tts/downloader.ts +64 -0
  1065. package/src/tts/index.ts +8 -0
  1066. package/src/tts/models.ts +137 -0
  1067. package/src/tts/player.ts +137 -0
  1068. package/src/tts/runtime.ts +21 -0
  1069. package/src/tts/streaming-player.ts +266 -0
  1070. package/src/tts/tts-client.ts +647 -0
  1071. package/src/tts/tts-protocol.ts +60 -0
  1072. package/src/tts/tts-worker.ts +505 -0
  1073. package/src/tts/vocalizer.ts +162 -0
  1074. package/src/tts/wav.ts +58 -0
  1075. package/src/tui/code-cell.ts +2 -7
  1076. package/src/tui/hyperlink.ts +40 -26
  1077. package/src/tui/output-block.ts +60 -108
  1078. package/src/tui/status-line.ts +5 -1
  1079. package/src/utils/block-context.ts +312 -0
  1080. package/src/utils/changelog.ts +27 -1
  1081. package/src/utils/clipboard.ts +91 -22
  1082. package/src/utils/commit-message-generator.ts +8 -3
  1083. package/src/utils/enhanced-paste.ts +230 -0
  1084. package/src/utils/file-mentions.ts +3 -1
  1085. package/src/utils/git.ts +315 -15
  1086. package/src/utils/image-loading.ts +65 -4
  1087. package/src/utils/session-color.ts +83 -9
  1088. package/src/utils/thinking-display.ts +37 -0
  1089. package/src/utils/title-generator.ts +73 -10
  1090. package/src/utils/tool-choice.ts +16 -0
  1091. package/src/utils/tools-manager.ts +19 -1
  1092. package/src/web/kagi.ts +28 -26
  1093. package/src/web/parallel.ts +7 -3
  1094. package/src/web/scrapers/arxiv.ts +1 -1
  1095. package/src/web/scrapers/github.ts +351 -3
  1096. package/src/web/scrapers/go-pkg.ts +1 -1
  1097. package/src/web/scrapers/iacr.ts +1 -1
  1098. package/src/web/scrapers/readthedocs.ts +1 -1
  1099. package/src/web/scrapers/twitter.ts +2 -1
  1100. package/src/web/scrapers/types.ts +87 -8
  1101. package/src/web/scrapers/wikipedia.ts +1 -1
  1102. package/src/web/scrapers/youtube.ts +9 -3
  1103. package/src/web/search/index.ts +15 -2
  1104. package/src/web/search/providers/anthropic.ts +62 -21
  1105. package/src/web/search/providers/base.ts +2 -1
  1106. package/src/web/search/providers/brave.ts +5 -2
  1107. package/src/web/search/providers/codex.ts +87 -51
  1108. package/src/web/search/providers/exa.ts +101 -10
  1109. package/src/web/search/providers/gemini.ts +49 -24
  1110. package/src/web/search/providers/jina.ts +15 -5
  1111. package/src/web/search/providers/kagi.ts +9 -2
  1112. package/src/web/search/providers/kimi.ts +45 -20
  1113. package/src/web/search/providers/parallel.ts +39 -24
  1114. package/src/web/search/providers/perplexity.ts +226 -63
  1115. package/src/web/search/providers/searxng.ts +19 -3
  1116. package/src/web/search/providers/synthetic.ts +16 -11
  1117. package/src/web/search/providers/tavily.ts +12 -9
  1118. package/src/web/search/providers/zai.ts +22 -9
  1119. package/src/web/search/render.ts +59 -64
  1120. package/src/web/search/types.ts +5 -1
  1121. package/dist/types/discovery/context-files.d.ts +0 -17
  1122. package/dist/types/exa/factory.d.ts +0 -13
  1123. package/dist/types/exa/render.d.ts +0 -19
  1124. package/dist/types/exa/researcher.d.ts +0 -9
  1125. package/dist/types/exa/search.d.ts +0 -9
  1126. package/dist/types/exa/websets.d.ts +0 -9
  1127. package/dist/types/export/html/template.generated.d.ts +0 -1
  1128. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  1129. package/dist/types/modes/components/status-line.d.ts +0 -77
  1130. package/dist/types/slash-commands/headless-plan.d.ts +0 -3
  1131. package/dist/types/stt/setup.d.ts +0 -18
  1132. package/scripts/generate-template.ts +0 -33
  1133. package/src/discovery/context-files.ts +0 -49
  1134. package/src/exa/factory.ts +0 -60
  1135. package/src/exa/render.ts +0 -244
  1136. package/src/exa/researcher.ts +0 -36
  1137. package/src/exa/search.ts +0 -47
  1138. package/src/exa/websets.ts +0 -248
  1139. package/src/export/html/template.generated.ts +0 -2
  1140. package/src/modes/components/session-observer-overlay.ts +0 -852
  1141. package/src/slash-commands/headless-plan.ts +0 -142
  1142. package/src/stt/setup.ts +0 -52
  1143. package/src/stt/transcribe.py +0 -70
  1144. /package/dist/types/extensibility/{legacy-pi-coding-agent-shim.d.ts → legacy-package-agent-shim.d.ts} +0 -0
  1145. /package/src/extensibility/{legacy-pi-coding-agent-shim.ts → legacy-package-agent-shim.ts} +0 -0
@@ -13,7 +13,6 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
 
16
- import * as crypto from "node:crypto";
17
16
  import * as fs from "node:fs";
18
17
  import * as os from "node:os";
19
18
  import * as path from "node:path";
@@ -29,6 +28,7 @@ import {
29
28
  type AgentState,
30
29
  type AgentTool,
31
30
  AppendOnlyContextManager,
31
+ type AsideMessage,
32
32
  resolveTelemetry,
33
33
  ThinkingLevel,
34
34
  } from "@prometheus-ai/agent-core";
@@ -49,12 +49,18 @@ import {
49
49
  generateBranchSummary,
50
50
  generateHandoff,
51
51
  prepareCompaction,
52
+ resolveThresholdTokens,
52
53
  type ShakeConfig,
53
54
  type ShakeRegion,
54
55
  type SummaryOptions,
55
56
  shouldCompact,
56
57
  } from "@prometheus-ai/agent-core/compaction";
57
- import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@prometheus-ai/agent-core/compaction/pruning";
58
+ import {
59
+ DEFAULT_PRUNE_CONFIG,
60
+ pruneSupersededToolResults,
61
+ pruneToolOutputs,
62
+ readToolSupersedeKey,
63
+ } from "@prometheus-ai/agent-core/compaction/pruning";
58
64
  import type { ProtectedToolMatcher } from "@prometheus-ai/agent-core/compaction/tool-protection";
59
65
  import type {
60
66
  AssistantMessage,
@@ -65,6 +71,9 @@ import type {
65
71
  Model,
66
72
  ProviderResponseMetadata,
67
73
  ProviderSessionState,
74
+ ResetCreditAccountStatus,
75
+ ResetCreditRedeemOutcome,
76
+ ResetCreditTarget,
68
77
  ServiceTier,
69
78
  SimpleStreamOptions,
70
79
  TextContent,
@@ -76,20 +85,22 @@ import type {
76
85
  import {
77
86
  calculateRateLimitBackoffMs,
78
87
  clearAnthropicFastModeFallback,
88
+ deriveClaudeDeviceId,
79
89
  Effort,
80
- getSupportedEfforts,
81
90
  isContextOverflow,
82
91
  isUsageLimitError,
83
- modelsAreEqual,
84
92
  parseRateLimitReason,
85
93
  resolveServiceTier,
86
94
  streamSimple,
87
95
  } from "@prometheus-ai/ai";
96
+ import { getSupportedEfforts } from "@prometheus-ai/catalog/model-thinking";
97
+ import { modelsAreEqual } from "@prometheus-ai/catalog/models";
88
98
  import type { InMemorySnapshotStore } from "@prometheus-ai/hashline";
89
99
  import { countTokens, MacOSPowerAssertion } from "@prometheus-ai/natives";
100
+ import * as snapcompact from "@prometheus-ai/snapcompact";
90
101
  import {
91
- APP_DISPLAY_NAME,
92
102
  extractRetryHint,
103
+ formatDuration,
93
104
  getAgentDbPath,
94
105
  getInstallId,
95
106
  isBunTestRuntime,
@@ -105,15 +116,18 @@ import { classifyDifficulty } from "../auto-thinking/classifier";
105
116
  import { reset as resetCapabilities } from "../capability";
106
117
  import type { Rule } from "../capability/rule";
107
118
  import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
108
- import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
119
+ import type { ModelRegistry } from "../config/model-registry";
109
120
  import {
110
121
  extractExplicitThinkingSelector,
122
+ filterAvailableModelsByEnabledPatterns,
111
123
  formatModelSelectorValue,
112
124
  formatModelString,
125
+ getModelMatchPreferences,
113
126
  parseModelString,
114
127
  type ResolvedModelRoleValue,
115
128
  resolveModelRoleValue,
116
129
  } from "../config/model-resolver";
130
+ import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
117
131
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
118
132
  import type { Settings, SkillsSettings } from "../config/settings";
119
133
  import { onAppendOnlyModeChanged } from "../config/settings";
@@ -129,7 +143,6 @@ import {
129
143
  } from "../eval/py/executor";
130
144
  import { defaultEvalSessionId } from "../eval/session-id";
131
145
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
132
- import { exportSessionToHtml } from "../export/html";
133
146
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
134
147
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
135
148
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
@@ -152,6 +165,7 @@ import type {
152
165
  TurnEndEvent,
153
166
  TurnStartEvent,
154
167
  } from "../extensibility/extensions";
168
+ import { createExtensionModelQuery } from "../extensibility/extensions/model-api";
155
169
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
156
170
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
157
171
  import type { HookCommandContext } from "../extensibility/hooks/types";
@@ -161,6 +175,7 @@ import { GoalRuntime } from "../goals/runtime";
161
175
  import type { Goal, GoalModeState } from "../goals/state";
162
176
  import type { HindsightSessionState } from "../hindsight/state";
163
177
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
178
+ import { IrcBus, type IrcMessage } from "../irc/bus";
164
179
  import { resolveMemoryBackend } from "../memory-backend";
165
180
  import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
166
181
  import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
@@ -172,8 +187,10 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
172
187
  import { createPlanReadMatcher } from "../plan-mode/plan-protection";
173
188
  import type { PlanModeState } from "../plan-mode/state";
174
189
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
190
+ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
175
191
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
176
192
  import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
193
+ import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
177
194
  import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
178
195
  import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
179
196
  import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
@@ -182,8 +199,12 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
182
199
  };
183
200
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
184
201
  import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
185
- import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
186
- import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
202
+ import {
203
+ deobfuscateSessionContext,
204
+ obfuscateProviderContext,
205
+ obfuscateProviderTools,
206
+ type SecretObfuscator,
207
+ } from "../secrets/obfuscator";
187
208
  import { invalidateHostMetadata } from "../ssh/connection-manager";
188
209
  import {
189
210
  AUTO_THINKING,
@@ -191,6 +212,7 @@ import {
191
212
  clampAutoThinkingEffort,
192
213
  resolveProvisionalAutoLevel,
193
214
  resolveThinkingLevelForModel,
215
+ shouldDisableReasoning,
194
216
  toReasoningEffort,
195
217
  } from "../thinking";
196
218
  import { shutdownTinyTitleClient } from "../tiny/title-client";
@@ -216,29 +238,33 @@ import { parseCommandArgs } from "../utils/command-args";
216
238
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
217
239
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
218
240
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
219
- import { buildNamedToolChoice } from "../utils/tool-choice";
241
+ import { normalizeModelContextImages } from "../utils/image-loading";
242
+ import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
220
243
  import type { AuthStorage } from "./auth-storage";
221
244
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
245
+ import {
246
+ type CodexAutoRedeemRedeemDecision,
247
+ defaultCodexAutoRedeemCoordinator,
248
+ evaluateCodexAutoRedeem,
249
+ shouldEvaluateCodexAutoRedeem,
250
+ shouldPromptCodexAutoRedeem,
251
+ } from "./codex-auto-reset";
222
252
  import {
223
253
  type BashExecutionMessage,
224
- type CompactionSummaryMessage,
225
254
  type CustomMessage,
226
255
  convertToLlm,
227
- type FileMentionMessage,
228
256
  type PythonExecutionMessage,
229
- readPendingDisplayTag,
257
+ readQueueChipText,
230
258
  SILENT_ABORT_MARKER,
259
+ SKILL_PROMPT_MESSAGE_TYPE,
231
260
  stripImagesFromMessage,
232
261
  } from "./messages";
262
+ import type { SessionContext } from "./session-context";
263
+ import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
233
264
  import { formatSessionDumpText } from "./session-dump-format";
234
- import type {
235
- BranchSummaryEntry,
236
- CompactionEntry,
237
- NewSessionOptions,
238
- SessionContext,
239
- SessionManager,
240
- } from "./session-manager";
241
- import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
265
+ import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions } from "./session-entries";
266
+ import { EPHEMERAL_MODEL_CHANGE_ROLE } from "./session-entries";
267
+ import type { SessionManager } from "./session-manager";
242
268
  import type { ShakeMode, ShakeResult } from "./shake-types";
243
269
  import { ToolChoiceQueue } from "./tool-choice-queue";
244
270
  import { YieldQueue } from "./yield-queue";
@@ -249,11 +275,11 @@ export type AgentSessionEvent =
249
275
  | {
250
276
  type: "auto_compaction_start";
251
277
  reason: "threshold" | "overflow" | "idle" | "incomplete";
252
- action: "context-full" | "handoff" | "shake";
278
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
253
279
  }
254
280
  | {
255
281
  type: "auto_compaction_end";
256
- action: "context-full" | "handoff" | "shake";
282
+ action: "context-full" | "handoff" | "shake" | "snapcompact";
257
283
  result: CompactionResult | undefined;
258
284
  aborted: boolean;
259
285
  willRetry: boolean;
@@ -282,9 +308,38 @@ export type AgentSessionEvent =
282
308
 
283
309
  /** Listener function for agent session events */
284
310
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
311
+ export type CommandMetadataChangedListener = () => void | Promise<void>;
285
312
  export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
286
313
 
287
314
  const EMPTY_STOP_MAX_RETRIES = 3;
315
+ const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
316
+ const RETRY_BACKOFF_JITTER_RATIO = 0.25;
317
+ /**
318
+ * Hysteresis band for the post-shake "did we actually create headroom?" check.
319
+ * Shake counts as having resolved threshold pressure only when residual context
320
+ * lands at or below `SHAKE_RECOVERY_BAND × threshold`. Re-checking against the
321
+ * raw threshold lets shake keep reclaiming a trickle of the previous turn's
322
+ * output and land just under the line every turn, sustaining the auto-continue
323
+ * dead loop reported in #2275.
324
+ */
325
+ const SHAKE_RECOVERY_BAND = 0.8;
326
+
327
+ function calculateRetryBackoffDelayMs(baseDelayMs: number, attempt: number): number {
328
+ const cappedDelayMs = Math.min(Math.max(0, baseDelayMs) * 2 ** Math.max(0, attempt - 1), RETRY_BACKOFF_MAX_DELAY_MS);
329
+ const jitter = 1 - Math.random() * RETRY_BACKOFF_JITTER_RATIO;
330
+ return cappedDelayMs * jitter;
331
+ }
332
+
333
+ /**
334
+ * Slack added past a sibling credential's block expiry before retrying, so
335
+ * the next getApiKey lands after the block has actually lapsed.
336
+ */
337
+ const SIBLING_UNBLOCK_BUFFER_MS = 1_000;
338
+ const NON_WHITESPACE_RE = /\S/;
339
+
340
+ function hasNonWhitespace(value: string): boolean {
341
+ return NON_WHITESPACE_RE.test(value);
342
+ }
288
343
 
289
344
  export interface AsyncJobSnapshot {
290
345
  running: AsyncJobSnapshotItem[];
@@ -302,6 +357,8 @@ export interface AgentSessionConfig {
302
357
  agent: Agent;
303
358
  sessionManager: SessionManager;
304
359
  settings: Settings;
360
+ /** Whether the caller explicitly requested yolo/auto-approve behavior for this session. */
361
+ autoApprove?: boolean;
305
362
  /** Models to cycle through with Ctrl+P (from --models flag) */
306
363
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
307
364
  /** Initial session thinking selector. */
@@ -382,8 +439,8 @@ export interface AgentSessionConfig {
382
439
  asyncJobManager?: AsyncJobManager;
383
440
  /** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
384
441
  agentId?: string;
385
- /** Shared agent registry (for forwarding IRC observations to the main session UI). */
386
- agentRegistry?: AgentRegistry;
442
+ /** Whether this session is the top-level agent or a subagent. Defaults to "main". */
443
+ agentKind?: "main" | "sub";
387
444
  /**
388
445
  * Override the provider-facing session ID for all API requests from this session.
389
446
  * When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
@@ -473,6 +530,12 @@ export interface SessionStats {
473
530
  cost: number;
474
531
  }
475
532
 
533
+ export interface FreshSessionResult {
534
+ previousSessionId: string;
535
+ sessionId: string;
536
+ closedProviderSessions: number;
537
+ }
538
+
476
539
  /** Internal marker for hook messages queued through the agent loop */
477
540
  // ============================================================================
478
541
  // Constants
@@ -496,6 +559,7 @@ interface ActiveRetryFallbackState {
496
559
  originalSelector: string;
497
560
  originalThinkingLevel: ConfiguredThinkingLevel | undefined;
498
561
  lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
562
+ pinned: boolean;
499
563
  }
500
564
 
501
565
  function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
@@ -520,24 +584,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
520
584
  return `${selector.provider}/${selector.id}`;
521
585
  }
522
586
 
523
- const IRC_REPLY_MAX_BYTES = 4096;
524
- export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
525
- const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
526
-
527
- export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
528
- if (!model) return undefined;
529
- return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
530
- ? ANTHROPIC_TOOL_CALL_BATCH_CAP
531
- : undefined;
532
- }
587
+ const EPHEMERAL_REPLY_MAX_BYTES = 4096;
533
588
 
534
589
  /**
535
- * Collapse degenerate IRC ephemeral replies before they hit the relay.
590
+ * Collapse degenerate ephemeral replies (/btw, /omfg side-channel turns).
536
591
  * Models occasionally loop on a single line (~16 reports of N-times-repeated
537
592
  * replies); compress runs longer than 3 down to one instance + `[…N×]`, then
538
593
  * cap at 4 KiB so a runaway reply can't flood the channel.
539
594
  */
540
- function dedupeIrcReply(text: string): string {
595
+ function dedupeEphemeralReply(text: string): string {
541
596
  if (!text) return text;
542
597
  const lines = text.split("\n");
543
598
  const out: string[] = [];
@@ -554,11 +609,11 @@ function dedupeIrcReply(text: string): string {
554
609
  i = j;
555
610
  }
556
611
  let result = out.join("\n");
557
- if (Buffer.byteLength(result, "utf8") > IRC_REPLY_MAX_BYTES) {
612
+ if (Buffer.byteLength(result, "utf8") > EPHEMERAL_REPLY_MAX_BYTES) {
558
613
  // Trim by characters until we're under the byte budget — handles multi-byte
559
614
  // glyphs at the boundary without splitting them.
560
615
  const suffix = "\n[…truncated]";
561
- const budget = IRC_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
616
+ const budget = EPHEMERAL_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
562
617
  while (Buffer.byteLength(result, "utf8") > budget) {
563
618
  result = result.slice(0, -1);
564
619
  }
@@ -603,14 +658,10 @@ function buildSessionMetadata(
603
658
  const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
604
659
  if (typeof accountUuid === "string" && accountUuid.length > 0) {
605
660
  userId.account_uuid = accountUuid;
606
- // Claude Code's `device_id` is a stable 64-hex install identifier. Use
607
- // prometheus's persistent install id as the root instead of deriving it from
608
- // `account_uuid`: logging into a different Claude account on the same
609
- // install should not make the device look new.
610
- userId.device_id = crypto
611
- .createHash("sha256")
612
- .update(`prometheus-claude-device-id-v1:${getInstallId()}`)
613
- .digest("hex");
661
+ // Claude Code's `device_id` is a stable 64-hex account-scoped install
662
+ // identifier. Include both prometheus's persistent install id and the Claude
663
+ // account UUID so two accounts on the same install do not share a device.
664
+ userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
614
665
  }
615
666
  }
616
667
  return { user_id: JSON.stringify(userId) };
@@ -811,12 +862,41 @@ function extractPermissionLocations(
811
862
  // AgentSession Class
812
863
  // ============================================================================
813
864
 
814
- /** Internal record stored in the steering/followUp display queues. The optional
815
- * `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
816
- * custom messages queued during streaming) and is matched by the custom-role
817
- * `message_start` dequeue branch; user-message pushes leave it undefined and
818
- * rely on the existing text-equality match. */
819
- type QueuedDisplayEntry = { text: string; tag?: string };
865
+ /** Entry returned by {@link AgentSession.clearQueue} / {@link AgentSession.popLastQueuedMessage}. */
866
+ export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
867
+
868
+ function queuedTextContent(message: AgentMessage): string | undefined {
869
+ if (!("content" in message)) return undefined;
870
+ const content = message.content;
871
+ if (typeof content === "string") return content;
872
+ return content.find((part): part is TextContent => part.type === "text")?.text;
873
+ }
874
+
875
+ function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
876
+ if (!("content" in message) || typeof message.content === "string") return undefined;
877
+ const images = message.content.filter(
878
+ (part): part is ImageContent =>
879
+ part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string",
880
+ );
881
+ return images.length > 0 ? images : undefined;
882
+ }
883
+
884
+ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
885
+ return !(message.role === "custom" && message.display === false);
886
+ }
887
+
888
+ function queueChipText(message: AgentMessage): string {
889
+ if (message.role === "custom") {
890
+ return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
891
+ }
892
+ const text = queuedTextContent(message) ?? "";
893
+ if (text) return text;
894
+ return queuedImageContent(message) ? "[Image]" : "";
895
+ }
896
+
897
+ function toRestoredQueuedMessage(message: AgentMessage): RestoredQueuedMessage {
898
+ return { text: queueChipText(message), images: queuedImageContent(message) };
899
+ }
820
900
 
821
901
  export class AgentSession {
822
902
  readonly agent: Agent;
@@ -824,6 +904,7 @@ export class AgentSession {
824
904
  readonly settings: Settings;
825
905
  readonly yieldQueue: YieldQueue;
826
906
  fileSnapshotStore?: InMemorySnapshotStore;
907
+ #autoApprove: boolean;
827
908
 
828
909
  #powerAssertion: MacOSPowerAssertion | undefined;
829
910
 
@@ -845,19 +926,12 @@ export class AgentSession {
845
926
  /** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
846
927
  #lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
847
928
  #eventListeners: AgentSessionEventListener[] = [];
929
+ #commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
848
930
 
849
- /** Tracks pending steering messages for UI display. Removed when delivered.
850
- * Entry shape: `{ text }` for plain-text steers (user-message dequeue
851
- * matches by `.text`); `{ text, tag }` for queued custom messages (skill
852
- * invocations dispatched while streaming) — the custom-role dequeue
853
- * matches by `.tag` so duplicate-args queued skills cannot collide. */
854
- #steeringMessages: QueuedDisplayEntry[] = [];
855
- /** Tracks pending follow-up messages for UI display. Removed when delivered.
856
- * See `#steeringMessages` for entry shape. */
857
- #followUpMessages: QueuedDisplayEntry[] = [];
858
931
  /** Messages queued to be included with the next user prompt as context ("asides"). */
859
932
  #pendingNextTurnMessages: CustomMessage[] = [];
860
933
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
934
+ #queuedMessageDrainScheduled = false;
861
935
  #planModeState: PlanModeState | undefined;
862
936
  #goalModeState: GoalModeState | undefined;
863
937
  #goalRuntime: GoalRuntime;
@@ -916,14 +990,14 @@ export class AgentSession {
916
990
  #activeEvalExecutions = new Set<Promise<unknown>>();
917
991
  #evalExecutionDisposing = false;
918
992
 
919
- // Background-channel IRC exchanges queued while the recipient was streaming.
920
- // Drained into history (via emitExternalEvent) once the recipient becomes idle.
921
- #pendingBackgroundExchanges: CustomMessage[][] = [];
922
- #scheduledBackgroundExchangeFlush = false;
923
- // Agent identity + registry for IRC relay forwarding to the main session UI.
993
+ // Incoming IRC messages received while a turn was streaming; drained as
994
+ // non-interrupting asides at the next step boundary (see the aside provider).
995
+ #pendingIrcAsides: CustomMessage[] = [];
996
+ // Agent identity (registry id) used for IRC routing and job ownership.
924
997
  #agentId: string | undefined;
925
- #agentRegistry: AgentRegistry | undefined;
998
+ #agentKind: "main" | "sub" = "main";
926
999
  #providerSessionId: string | undefined;
1000
+ #freshProviderSessionId: string | undefined;
927
1001
  #isDisposed = false;
928
1002
  // Extension system
929
1003
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1001,10 +1075,6 @@ export class AgentSession {
1001
1075
  * without producing an aborted message_end). */
1002
1076
  #planCompactAbortPending = false;
1003
1077
 
1004
- /** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
1005
- * combined with `Date.now()` so tags stay unique even across rapid
1006
- * same-tick enqueues. */
1007
- #customDisplayTagCounter = 0;
1008
1078
  #postPromptTasks = new Set<Promise<unknown>>();
1009
1079
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
1010
1080
  #postPromptTasksResolve: (() => void) | undefined = undefined;
@@ -1017,6 +1087,7 @@ export class AgentSession {
1017
1087
 
1018
1088
  #streamingEditFileCache = new Map<string, string>();
1019
1089
  #promptInFlightCount = 0;
1090
+ #abortInProgress = false;
1020
1091
  // Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
1021
1092
  // Internal extension hooks and post-emit work (auto-retry, auto-compaction, todo
1022
1093
  // checks in #handleAgentEvent) still fire on the original schedule — only the
@@ -1048,7 +1119,7 @@ export class AgentSession {
1048
1119
  if (!idle && !system && !user && !display) return;
1049
1120
  try {
1050
1121
  this.#powerAssertion = MacOSPowerAssertion.start({
1051
- reason: `${APP_DISPLAY_NAME} agent session`,
1122
+ reason: "Prometheus agent session",
1052
1123
  idle,
1053
1124
  system,
1054
1125
  user,
@@ -1082,17 +1153,25 @@ export class AgentSession {
1082
1153
  if (this.#promptInFlightCount === 0) {
1083
1154
  this.#releasePowerAssertion();
1084
1155
  this.#flushPendingAgentEnd();
1156
+ this.#drainStrandedQueuedMessages();
1085
1157
  }
1086
1158
  }
1087
1159
 
1160
+ /** A steer/follow-up can land after the agent loop's final queue poll, or
1161
+ * after an abort stops an auto-continued queued turn. In both cases the
1162
+ * agent-core queue still owns the message, but no loop is left to poll it.
1163
+ * Runs whenever the session settles; the guard makes it a no-op when the
1164
+ * queue was consumed normally or a new turn already started. */
1165
+ #drainStrandedQueuedMessages(): void {
1166
+ if (this.#abortInProgress) return;
1167
+ this.#scheduleQueuedMessageDrain();
1168
+ }
1169
+
1088
1170
  #resetInFlight(): void {
1089
1171
  this.#promptInFlightCount = 0;
1090
1172
  this.#releasePowerAssertion();
1091
1173
  this.#flushPendingAgentEnd();
1092
- }
1093
-
1094
- #syncToolCallBatchCap(model: Model | undefined = this.model): void {
1095
- this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
1174
+ this.#drainStrandedQueuedMessages();
1096
1175
  }
1097
1176
 
1098
1177
  #flushPendingAgentEnd(): void {
@@ -1106,6 +1185,7 @@ export class AgentSession {
1106
1185
  this.agent = config.agent;
1107
1186
  this.sessionManager = config.sessionManager;
1108
1187
  this.settings = config.settings;
1188
+ this.#autoApprove = config.autoApprove === true;
1109
1189
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1110
1190
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1111
1191
  this.#parentEvalSessionId = config.parentEvalSessionId;
@@ -1121,6 +1201,7 @@ export class AgentSession {
1121
1201
  } else {
1122
1202
  this.#thinkingLevel = config.thinkingLevel;
1123
1203
  }
1204
+ this.#applyThinkingLevelToAgent(this.#thinkingLevel);
1124
1205
  this.#promptTemplates = config.promptTemplates ?? [];
1125
1206
  this.#slashCommands = config.slashCommands ?? [];
1126
1207
  this.#extensionRunner = config.extensionRunner;
@@ -1163,7 +1244,6 @@ export class AgentSession {
1163
1244
  this.agent.setRawSseEventInterceptor(this.#onSseEvent);
1164
1245
  this.yieldQueue = new YieldQueue({
1165
1246
  isStreaming: () => this.isStreaming,
1166
- injectStreaming: message => this.agent.followUp(message),
1167
1247
  injectIdle: async messages => {
1168
1248
  const first = messages[0];
1169
1249
  if (!first) return;
@@ -1178,7 +1258,16 @@ export class AgentSession {
1178
1258
  );
1179
1259
  },
1180
1260
  });
1181
- this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
1261
+ // Background-job completions / late diagnostics are pulled into the run at
1262
+ // each step boundary as non-interrupting asides (see Agent.getAsideMessages),
1263
+ // so they reach the model between requests without waiting for a yield.
1264
+ this.agent.setAsideMessageProvider(() => {
1265
+ const pendingIrc = this.#pendingIrcAsides;
1266
+ this.#pendingIrcAsides = [];
1267
+ const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
1268
+ thunks.push(...this.yieldQueue.drainLazy());
1269
+ return thunks;
1270
+ });
1182
1271
  this.#convertToLlm = config.convertToLlm ?? convertToLlm;
1183
1272
  this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
1184
1273
  this.#getMcpServerInstructions = config.getMcpServerInstructions;
@@ -1209,9 +1298,8 @@ export class AgentSession {
1209
1298
  this.#ttsrManager = config.ttsrManager;
1210
1299
  this.#obfuscator = config.obfuscator;
1211
1300
  this.#agentId = config.agentId;
1212
- this.#agentRegistry = config.agentRegistry;
1301
+ this.#agentKind = config.agentKind ?? "main";
1213
1302
  this.#providerSessionId = config.providerSessionId;
1214
- this.#syncToolCallBatchCap();
1215
1303
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1216
1304
  const event: AgentEvent = {
1217
1305
  type: "message_update",
@@ -1277,9 +1365,22 @@ export class AgentSession {
1277
1365
  return this.#modelRegistry;
1278
1366
  }
1279
1367
 
1368
+ get asyncJobManager(): AsyncJobManager | undefined {
1369
+ return this.#asyncJobManager;
1370
+ }
1371
+
1372
+ getAgentId(): string | undefined {
1373
+ return this.#agentId;
1374
+ }
1375
+
1280
1376
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1281
1377
  nextToolChoice(): ToolChoice | undefined {
1282
- return this.#toolChoiceQueue.nextToolChoice();
1378
+ const choice = this.#toolChoiceQueue.nextToolChoice();
1379
+ if (isToolChoiceActive(choice, this.agent.state.tools)) {
1380
+ return choice;
1381
+ }
1382
+ this.#toolChoiceQueue.reject("unavailable");
1383
+ return undefined;
1283
1384
  }
1284
1385
 
1285
1386
  /**
@@ -1356,6 +1457,11 @@ export class AgentSession {
1356
1457
  return this.#ttsrManager;
1357
1458
  }
1358
1459
 
1460
+ /** Secret obfuscator, when secrets are configured; /share redaction reuses it. */
1461
+ get obfuscator(): SecretObfuscator | undefined {
1462
+ return this.#obfuscator;
1463
+ }
1464
+
1359
1465
  /** Whether a TTSR abort is pending (stream was aborted to inject rules) */
1360
1466
  get isTtsrAbortPending(): boolean {
1361
1467
  return this.#ttsrAbortPending;
@@ -1382,28 +1488,6 @@ export class AgentSession {
1382
1488
  this.#planCompactAbortPending = false;
1383
1489
  }
1384
1490
 
1385
- /** Register a compact display string for a custom message that the caller is
1386
- * about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
1387
- * Returns a stable tag the caller MUST embed in
1388
- * `CustomMessage.details.__pendingDisplayTag` so the agent-side
1389
- * `message_start` handler can remove the matching display entry when the
1390
- * queued message is consumed.
1391
- *
1392
- * Does NOT push to the agent's steering/followUp queue — that happens
1393
- * separately inside `sendCustomMessage`. */
1394
- enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
1395
- const tag = `prometheus-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
1396
- const displayText = text.trim();
1397
- if (!displayText) return tag;
1398
- const entry: QueuedDisplayEntry = { text: displayText, tag };
1399
- if (mode === "steer") {
1400
- this.#steeringMessages.push(entry);
1401
- } else {
1402
- this.#followUpMessages.push(entry);
1403
- }
1404
- return tag;
1405
- }
1406
-
1407
1491
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
1408
1492
  const manager = this.#asyncJobManager;
1409
1493
  if (!manager) return null;
@@ -1525,45 +1609,6 @@ export class AgentSession {
1525
1609
 
1526
1610
  /** Internal handler for agent events - shared by subscribe and reconnect */
1527
1611
  #handleAgentEvent = async (event: AgentEvent): Promise<void> => {
1528
- // When a user message starts, check if it's from either queue and remove it BEFORE emitting
1529
- // This ensures the UI sees the updated queue state
1530
- if (event.type === "message_start" && event.message.role === "user") {
1531
- const messageText = this.#getUserMessageText(event.message);
1532
- if (messageText) {
1533
- // Check steering queue first (match by .text on tagged records)
1534
- const steeringIndex = this.#steeringMessages.findIndex(e => e.text === messageText);
1535
- if (steeringIndex !== -1) {
1536
- this.#steeringMessages.splice(steeringIndex, 1);
1537
- } else {
1538
- // Check follow-up queue
1539
- const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
1540
- if (followUpIndex !== -1) {
1541
- this.#followUpMessages.splice(followUpIndex, 1);
1542
- }
1543
- }
1544
- }
1545
- }
1546
-
1547
- // Tag-based dequeue for custom messages (skills queued via promptCustomMessage).
1548
- // The InputController attached a stable tag via CustomMessage.details when it
1549
- // registered the display chip; pull it back here to remove the matching entry
1550
- // from the pending bar atomically with the agent's queue consumption. Match by
1551
- // tag (not text) — two queued skills with identical args cannot collide.
1552
- if (event.type === "message_start" && event.message.role === "custom") {
1553
- const tag = readPendingDisplayTag(event.message.details);
1554
- if (tag) {
1555
- const steerIdx = this.#steeringMessages.findIndex(e => e.tag === tag);
1556
- if (steerIdx !== -1) {
1557
- this.#steeringMessages.splice(steerIdx, 1);
1558
- } else {
1559
- const followUpIdx = this.#followUpMessages.findIndex(e => e.tag === tag);
1560
- if (followUpIdx !== -1) {
1561
- this.#followUpMessages.splice(followUpIdx, 1);
1562
- }
1563
- }
1564
- }
1565
- }
1566
-
1567
1612
  // Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
1568
1613
  // persisted message BEFORE the obfuscator's display-side copy below.
1569
1614
  // Invariant (must hold across refactors): this branch precedes the
@@ -1666,89 +1711,18 @@ export class AgentSession {
1666
1711
  }
1667
1712
 
1668
1713
  if (matchContext && "delta" in assistantEvent) {
1714
+ const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
1669
1715
  const matches = this.#checkTtsrStream(assistantEvent.delta, matchContext, streamingToolCall);
1670
- if (matches.length > 0) {
1671
- // Decide first: a non-interrupting tool-source match attaches to the
1672
- // specific tool call's result instead of driving a loop-wide follow-up.
1673
- const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
1674
- const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
1675
- if (perToolId) {
1676
- this.#addPerToolTtsrInjections(perToolId, matches);
1677
- this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1678
- } else {
1679
- // Queue rules for injection; mark as injected only after successful enqueue.
1680
- this.#addPendingTtsrInjections(matches);
1681
-
1682
- if (shouldInterrupt) {
1683
- // Abort the stream immediately — do not gate on extension callbacks
1684
- this.#ttsrAbortPending = true;
1685
- this.#ensureTtsrResumePromise();
1686
- this.agent.abort();
1687
- // Notify extensions (fire-and-forget, does not block abort)
1688
- this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1689
- // Schedule retry after a short delay
1690
- const retryToken = ++this.#ttsrRetryToken;
1691
- const generation = this.#promptGeneration;
1692
- const targetMessageTimestamp =
1693
- event.message.role === "assistant" ? event.message.timestamp : undefined;
1694
- this.#schedulePostPromptTask(
1695
- async () => {
1696
- if (this.#ttsrRetryToken !== retryToken) {
1697
- this.#resolveTtsrResume();
1698
- return;
1699
- }
1700
-
1701
- const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
1702
- if (
1703
- !this.#ttsrAbortPending ||
1704
- this.#promptGeneration !== generation ||
1705
- targetAssistantIndex === -1
1706
- ) {
1707
- this.#ttsrAbortPending = false;
1708
- this.#pendingTtsrInjections = [];
1709
- this.#perToolTtsrInjections.clear();
1710
- this.#resolveTtsrResume();
1711
- return;
1712
- }
1713
- this.#ttsrAbortPending = false;
1714
- this.#perToolTtsrInjections.clear();
1715
- const ttsrSettings = this.#ttsrManager?.getSettings();
1716
- if (ttsrSettings?.contextMode === "discard") {
1717
- // Remove the partial/aborted assistant turn from agent state
1718
- this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
1719
- }
1720
- // Inject TTSR rules as system reminder before retry
1721
- const injection = this.#getTtsrInjectionContent();
1722
- if (injection) {
1723
- const details = { rules: injection.rules.map(rule => rule.name) };
1724
- this.agent.appendMessage({
1725
- role: "custom",
1726
- customType: "ttsr-injection",
1727
- content: injection.content,
1728
- display: false,
1729
- details,
1730
- attribution: "agent",
1731
- timestamp: Date.now(),
1732
- });
1733
- this.sessionManager.appendCustomMessageEntry(
1734
- "ttsr-injection",
1735
- injection.content,
1736
- false,
1737
- details,
1738
- "agent",
1739
- );
1740
- this.#markTtsrInjected(details.rules);
1741
- }
1742
- try {
1743
- await this.agent.continue();
1744
- } catch {
1745
- this.#resolveTtsrResume();
1746
- }
1747
- },
1748
- { delayMs: 50 },
1749
- );
1750
- return;
1751
- }
1716
+ if (matches.length > 0 && this.#handleTtsrMatches(matches, matchContext, targetMessageTimestamp)) {
1717
+ return;
1718
+ }
1719
+ // ast-grep `astCondition` rules match against the reconstructed edit/write
1720
+ // snapshot, which only exists for tool argument streams. The native worker
1721
+ // call is async, so this path is awaited and self-throttled by the manager.
1722
+ if (matchContext.source === "tool" && this.#ttsrManager?.hasAstRules()) {
1723
+ const astMatches = await this.#checkTtsrAstStream(matchContext, streamingToolCall);
1724
+ if (astMatches.length > 0 && this.#handleTtsrMatches(astMatches, matchContext, targetMessageTimestamp)) {
1725
+ return;
1752
1726
  }
1753
1727
  }
1754
1728
  }
@@ -1944,6 +1918,11 @@ export class AgentSession {
1944
1918
  return;
1945
1919
  }
1946
1920
 
1921
+ // A deliberate abort should settle the current turn, not trigger queued continuations.
1922
+ if (msg.stopReason === "aborted") {
1923
+ this.#resolveRetry();
1924
+ return;
1925
+ }
1947
1926
  // Check for retryable errors first (overloaded, rate limit, server errors)
1948
1927
  if (this.#isRetryableError(msg)) {
1949
1928
  const didRetry = await this.#handleRetryableError(msg);
@@ -1966,7 +1945,7 @@ export class AgentSession {
1966
1945
  if (compactionDeferredHandoff) {
1967
1946
  return;
1968
1947
  }
1969
- if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
1948
+ if (msg.stopReason !== "error") {
1970
1949
  if (this.#enforceRewindBeforeYield()) {
1971
1950
  return;
1972
1951
  }
@@ -2062,13 +2041,13 @@ export class AgentSession {
2062
2041
  onError?: () => void;
2063
2042
  }): void {
2064
2043
  this.#schedulePostPromptTask(
2065
- async () => {
2044
+ async signal => {
2066
2045
  // Defense in depth: if compaction/handoff slipped onto the post-prompt queue
2067
2046
  // alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
2068
2047
  // streaming turn — agent.continue() here would race the handoff's session
2069
2048
  // reset. The first-class fix is in #checkCompaction/the agent_end handler,
2070
2049
  // but this guard catches anything that bypasses that path.
2071
- if (this.isCompacting || this.isGeneratingHandoff) {
2050
+ if (signal.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) {
2072
2051
  options?.onSkip?.();
2073
2052
  return;
2074
2053
  }
@@ -2076,14 +2055,21 @@ export class AgentSession {
2076
2055
  options?.onSkip?.();
2077
2056
  return;
2078
2057
  }
2058
+ this.#beginInFlight();
2079
2059
  try {
2080
2060
  await this.#maybeRestoreRetryFallbackPrimary();
2061
+ if (signal.aborted || this.#isDisposed) {
2062
+ options?.onSkip?.();
2063
+ return;
2064
+ }
2081
2065
  await this.agent.continue();
2082
2066
  } catch (error) {
2083
2067
  logger.warn("agent.continue failed after scheduling", {
2084
2068
  error: error instanceof Error ? error.message : String(error),
2085
2069
  });
2086
2070
  options?.onError?.();
2071
+ } finally {
2072
+ this.#endInFlight();
2087
2073
  }
2088
2074
  },
2089
2075
  {
@@ -2139,8 +2125,13 @@ export class AgentSession {
2139
2125
  * and fire-and-forget `agent.continue()` may still be streaming after
2140
2126
  * the TTSR resume gate resolves.
2141
2127
  */
2142
- async #waitForPostPromptRecovery(): Promise<void> {
2128
+ async #waitForPostPromptRecovery(generation?: number): Promise<void> {
2143
2129
  while (true) {
2130
+ // An abort bumps #promptGeneration. When this wait runs on behalf of a
2131
+ // specific prompt turn, stop as soon as that turn has been superseded:
2132
+ // its promise must resolve on the abort, not block on a queued
2133
+ // steer/follow-up that the post-abort drain starts as a fresh turn.
2134
+ if (generation !== undefined && this.#promptGeneration !== generation) return;
2144
2135
  if (this.#retryPromise) {
2145
2136
  await this.#retryPromise;
2146
2137
  continue;
@@ -2164,6 +2155,12 @@ export class AgentSession {
2164
2155
  }
2165
2156
  }
2166
2157
 
2158
+ #formatTtsrAbortReason(rules: Rule[]): string {
2159
+ const label = rules.length === 1 ? "rule" : "rules";
2160
+ const ruleNames = rules.map(rule => rule.name).join(", ");
2161
+ return `TTSR matched ${label}: ${ruleNames}`;
2162
+ }
2163
+
2167
2164
  /** Get TTSR injection payload and clear pending injections. */
2168
2165
  #getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
2169
2166
  if (this.#pendingTtsrInjections.length === 0) return undefined;
@@ -2187,13 +2184,20 @@ export class AgentSession {
2187
2184
  * project, `~`-relative when it lives under home, else the raw path.
2188
2185
  */
2189
2186
  #displayRulePath(rulePath: string): string {
2190
- const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
2187
+ const cwdRel =
2188
+ relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
2189
+ this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
2191
2190
  if (cwdRel) return cwdRel;
2192
2191
  const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
2193
2192
  if (homeRel) return `~/${homeRel}`;
2194
2193
  return rulePath;
2195
2194
  }
2196
2195
 
2196
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
2197
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
2198
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
2199
+ }
2200
+
2197
2201
  #addPendingTtsrInjections(rules: Rule[]): void {
2198
2202
  const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
2199
2203
  for (const rule of rules) {
@@ -2407,19 +2411,134 @@ export class AgentSession {
2407
2411
  if (!manager) {
2408
2412
  return [];
2409
2413
  }
2410
- if (toolCall) {
2411
- const tools = this.agent.state.tools;
2412
- const tool =
2413
- tools.find(t => t.name === toolCall.name) ??
2414
- tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
2415
- const digest = tool?.matcherDigest?.(toolCall.arguments ?? {});
2416
- if (digest !== undefined) {
2417
- return manager.checkSnapshot(digest, matchContext);
2418
- }
2414
+ const digest = this.#resolveTtsrMatcherDigest(toolCall);
2415
+ if (digest !== undefined) {
2416
+ return manager.checkSnapshot(digest, matchContext);
2419
2417
  }
2420
2418
  return manager.checkDelta(delta, matchContext);
2421
2419
  }
2422
2420
 
2421
+ /** Reconstruct the tool's normalized source snapshot via its `matcherDigest`, if any. */
2422
+ #resolveTtsrMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
2423
+ if (!toolCall) {
2424
+ return undefined;
2425
+ }
2426
+ const tools = this.agent.state.tools;
2427
+ const tool =
2428
+ tools.find(t => t.name === toolCall.name) ??
2429
+ tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
2430
+ return tool?.matcherDigest?.(toolCall.arguments ?? {});
2431
+ }
2432
+
2433
+ /**
2434
+ * Match ast-grep `astCondition` rules against the reconstructed tool snapshot.
2435
+ *
2436
+ * Only edit/write tool streams expose a `matcherDigest`, which is the real source
2437
+ * the call introduces; AST matching needs that (and a language inferred from the
2438
+ * path argument), so non-digest streams never produce AST matches.
2439
+ */
2440
+ async #checkTtsrAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
2441
+ const manager = this.#ttsrManager;
2442
+ if (!manager) {
2443
+ return [];
2444
+ }
2445
+ const digest = this.#resolveTtsrMatcherDigest(toolCall);
2446
+ if (digest === undefined) {
2447
+ return [];
2448
+ }
2449
+ return manager.checkAstSnapshot(digest, matchContext);
2450
+ }
2451
+
2452
+ /**
2453
+ * Route TTSR matches to either a per-tool injection or a stream-interrupting
2454
+ * retry. Returns true when the stream was aborted and the caller should stop
2455
+ * processing this event.
2456
+ */
2457
+ #handleTtsrMatches(
2458
+ matches: Rule[],
2459
+ matchContext: TtsrMatchContext,
2460
+ targetMessageTimestamp: number | undefined,
2461
+ ): boolean {
2462
+ // Decide first: a non-interrupting tool-source match attaches to the
2463
+ // specific tool call's result instead of driving a loop-wide follow-up.
2464
+ const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
2465
+ const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
2466
+ if (perToolId) {
2467
+ this.#addPerToolTtsrInjections(perToolId, matches);
2468
+ this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
2469
+ return false;
2470
+ }
2471
+
2472
+ // Queue rules for injection; mark as injected only after successful enqueue.
2473
+ this.#addPendingTtsrInjections(matches);
2474
+ if (!shouldInterrupt) {
2475
+ return false;
2476
+ }
2477
+
2478
+ // Abort the stream immediately — do not gate on extension callbacks
2479
+ this.#ttsrAbortPending = true;
2480
+ this.#ensureTtsrResumePromise();
2481
+ this.agent.abort(this.#formatTtsrAbortReason(matches));
2482
+ // Notify extensions (fire-and-forget, does not block abort)
2483
+ this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
2484
+ // Schedule retry after a short delay
2485
+ const retryToken = ++this.#ttsrRetryToken;
2486
+ const generation = this.#promptGeneration;
2487
+ this.#schedulePostPromptTask(
2488
+ async () => {
2489
+ if (this.#ttsrRetryToken !== retryToken) {
2490
+ this.#resolveTtsrResume();
2491
+ return;
2492
+ }
2493
+
2494
+ const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
2495
+ if (!this.#ttsrAbortPending || this.#promptGeneration !== generation || targetAssistantIndex === -1) {
2496
+ this.#ttsrAbortPending = false;
2497
+ this.#pendingTtsrInjections = [];
2498
+ this.#perToolTtsrInjections.clear();
2499
+ this.#resolveTtsrResume();
2500
+ return;
2501
+ }
2502
+ this.#ttsrAbortPending = false;
2503
+ this.#perToolTtsrInjections.clear();
2504
+ const ttsrSettings = this.#ttsrManager?.getSettings();
2505
+ if (ttsrSettings?.contextMode === "discard") {
2506
+ // Remove the partial/aborted assistant turn from agent state
2507
+ this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
2508
+ }
2509
+ // Inject TTSR rules as system reminder before retry
2510
+ const injection = this.#getTtsrInjectionContent();
2511
+ if (injection) {
2512
+ const details = { rules: injection.rules.map(rule => rule.name) };
2513
+ this.agent.appendMessage({
2514
+ role: "custom",
2515
+ customType: "ttsr-injection",
2516
+ content: injection.content,
2517
+ display: false,
2518
+ details,
2519
+ attribution: "agent",
2520
+ timestamp: Date.now(),
2521
+ });
2522
+ this.sessionManager.appendCustomMessageEntry(
2523
+ "ttsr-injection",
2524
+ injection.content,
2525
+ false,
2526
+ details,
2527
+ "agent",
2528
+ );
2529
+ this.#markTtsrInjected(details.rules);
2530
+ }
2531
+ try {
2532
+ await this.agent.continue();
2533
+ } catch {
2534
+ this.#resolveTtsrResume();
2535
+ }
2536
+ },
2537
+ { delayMs: 50 },
2538
+ );
2539
+ return true;
2540
+ }
2541
+
2423
2542
  /** Extract path-like arguments from tool call payload for TTSR glob matching. */
2424
2543
  #extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
2425
2544
  if (!args || typeof args !== "object" || Array.isArray(args)) {
@@ -2474,18 +2593,6 @@ export class AgentSession {
2474
2593
 
2475
2594
  return Array.from(candidates);
2476
2595
  }
2477
- /** Extract text content from a message */
2478
- #getUserMessageText(message: Message): string {
2479
- if (message.role !== "user") return "";
2480
- const content = message.content;
2481
- if (typeof content === "string") return content;
2482
- const textBlocks = content.filter(c => c.type === "text");
2483
- const text = textBlocks.map(c => (c as TextContent).text).join("");
2484
- if (text.length > 0) return text;
2485
- const hasImages = content.some(c => c.type === "image");
2486
- return hasImages ? "[Image]" : "";
2487
- }
2488
-
2489
2596
  /** Find the last assistant message in agent state (including aborted ones) */
2490
2597
  #findLastAssistantMessage(): AssistantMessage | undefined {
2491
2598
  const messages = this.agent.state.messages;
@@ -2927,6 +3034,27 @@ export class AgentSession {
2927
3034
  };
2928
3035
  }
2929
3036
 
3037
+ subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void {
3038
+ this.#commandMetadataChangedListeners.push(listener);
3039
+ return () => {
3040
+ const index = this.#commandMetadataChangedListeners.indexOf(listener);
3041
+ if (index !== -1) {
3042
+ this.#commandMetadataChangedListeners.splice(index, 1);
3043
+ }
3044
+ };
3045
+ }
3046
+
3047
+ #notifyCommandMetadataChanged(): void {
3048
+ const listeners = [...this.#commandMetadataChangedListeners];
3049
+ for (const listener of listeners) {
3050
+ try {
3051
+ void listener();
3052
+ } catch (err) {
3053
+ logger.error("Command metadata listener threw", { err });
3054
+ }
3055
+ }
3056
+ }
3057
+
2930
3058
  /**
2931
3059
  * Temporarily disconnect from agent events.
2932
3060
  * User listeners are preserved and will receive events again after resubscribe().
@@ -2948,19 +3076,23 @@ export class AgentSession {
2948
3076
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
2949
3077
  }
2950
3078
 
3079
+ #activeProviderSessionId(sessionId?: string): string {
3080
+ return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
3081
+ }
3082
+
2951
3083
  /**
2952
3084
  * Set agent.sessionId from the session manager and install a dynamic
2953
3085
  * metadata resolver so every Anthropic API request carries
2954
3086
  * `metadata.user_id` shaped like real Claude Code's `getAPIMetadata` output:
2955
3087
  * `{ session_id, account_uuid, device_id }`. `account_uuid` is included only
2956
3088
  * when an Anthropic OAuth credential with a known account UUID is loaded;
2957
- * `device_id` is derived from the persistent prometheus install id. Resolving live
2958
- * keeps the value in sync with auth-state changes (login/logout, token
2959
- * refresh that surfaces a new account uuid) without needing to re-call
2960
- * `#syncAgentSessionId()` on every such event.
3089
+ * `device_id` is derived from both the persistent prometheus install id and that
3090
+ * account UUID. Resolving live keeps the value in sync with auth-state changes
3091
+ * (login/logout, token refresh that surfaces a new account UUID) without
3092
+ * needing to re-call `#syncAgentSessionId()` on every such event.
2961
3093
  */
2962
3094
  #syncAgentSessionId(sessionId?: string): void {
2963
- const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
3095
+ const sid = this.#activeProviderSessionId(sessionId);
2964
3096
  this.agent.sessionId = sid;
2965
3097
  this.agent.setMetadataResolver((provider: string) =>
2966
3098
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
@@ -2968,14 +3100,14 @@ export class AgentSession {
2968
3100
  }
2969
3101
 
2970
3102
  #rekeyHindsightMemoryForCurrentSessionId(): void {
2971
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
3103
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2972
3104
  const sid = this.agent.sessionId;
2973
3105
  if (!sid) return;
2974
3106
  this.getHindsightSessionState()?.setSessionId(sid);
2975
3107
  }
2976
3108
 
2977
3109
  #rekeyMnemopiMemoryForCurrentSessionId(): void {
2978
- if (resolveMemoryBackend(this.settings).id !== "prometheus-memory") return;
3110
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2979
3111
  const sid = this.agent.sessionId;
2980
3112
  if (!sid) return;
2981
3113
  this.getMnemopiSessionState()?.setSessionId(sid);
@@ -2983,29 +3115,48 @@ export class AgentSession {
2983
3115
 
2984
3116
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
2985
3117
  #resetHindsightConversationTrackingIfHindsight(): void {
2986
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
3118
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2987
3119
  const state = this.getHindsightSessionState();
2988
3120
  if (!state || state.aliasOf) return;
2989
3121
  state.resetConversationTracking();
2990
3122
  }
2991
3123
 
2992
3124
  #resetMnemopiConversationTrackingIfMnemopi(): void {
2993
- if (resolveMemoryBackend(this.settings).id !== "prometheus-memory") return;
3125
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2994
3126
  const state = this.getMnemopiSessionState();
2995
3127
  if (!state || state.aliasOf) return;
2996
3128
  state.resetConversationTracking();
2997
3129
  }
2998
3130
 
3131
+ /** True once dispose() has begun; deferred background work (e.g. the deferred
3132
+ * MCP discovery task in sdk.ts) must not touch the session past this point. */
3133
+ get isDisposed(): boolean {
3134
+ return this.#isDisposed;
3135
+ }
3136
+
2999
3137
  /**
3000
- * Remove all listeners, flush pending writes, and disconnect from agent.
3001
- * Call this when completely done with the session.
3138
+ * Synchronously mark the session as disposing so new work is rejected
3139
+ * immediately: Python/eval starts throw, queued asides are dropped, and the
3140
+ * aside provider is detached. Idempotent; `dispose()` runs it first.
3141
+ *
3142
+ * Wrappers that await other teardown before delegating to `dispose()` MUST
3143
+ * call this before their first await — otherwise work started in that async
3144
+ * gap slips past the disposal guards.
3002
3145
  */
3003
- async dispose(): Promise<void> {
3146
+ beginDispose(): void {
3004
3147
  this.#isDisposed = true;
3005
- this.#pendingBackgroundExchanges = [];
3148
+ this.#pendingIrcAsides = [];
3006
3149
  this.yieldQueue.clear();
3007
- this.agent.setOnBeforeYield(undefined);
3150
+ this.agent.setAsideMessageProvider(undefined);
3008
3151
  this.#evalExecutionDisposing = true;
3152
+ }
3153
+
3154
+ /**
3155
+ * Remove all listeners, flush pending writes, and disconnect from agent.
3156
+ * Call this when completely done with the session.
3157
+ */
3158
+ async dispose(): Promise<void> {
3159
+ this.beginDispose();
3009
3160
  try {
3010
3161
  if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
3011
3162
  await this.#extensionRunner.emit({ type: "session_shutdown" });
@@ -3027,8 +3178,9 @@ export class AgentSession {
3027
3178
  // session's dispose.
3028
3179
  this.abortRetry();
3029
3180
  this.abortCompaction();
3181
+ const postPromptDrain = this.#cancelPostPromptTasks();
3030
3182
  this.agent.abort();
3031
- await this.#cancelPostPromptTasks();
3183
+ await postPromptDrain;
3032
3184
  // Cancel jobs this agent registered so a subagent's teardown doesn't
3033
3185
  // leak its background bash/task work into the parent's manager. Only
3034
3186
  // the session that owns the manager goes on to dispose it (which itself
@@ -3065,7 +3217,7 @@ export class AgentSession {
3065
3217
  this.setHindsightSessionState(undefined);
3066
3218
  hindsightState?.dispose();
3067
3219
  const mnemopiState = setMnemopiSessionState(this, undefined);
3068
- mnemopiState?.dispose();
3220
+ await mnemopiState?.dispose();
3069
3221
  this.#disconnectFromAgent();
3070
3222
  if (this.#unsubscribeAppendOnly) {
3071
3223
  this.#unsubscribeAppendOnly();
@@ -3090,6 +3242,23 @@ export class AgentSession {
3090
3242
  this.#providerSessionState.clear();
3091
3243
  }
3092
3244
 
3245
+ freshSession(): FreshSessionResult | undefined {
3246
+ if (this.isStreaming) return undefined;
3247
+ const previousSessionId = this.sessionId;
3248
+ const closedProviderSessions = this.#providerSessionState.size;
3249
+ this.#closeAllProviderSessions("fresh session");
3250
+ this.#freshProviderSessionId = Bun.randomUUIDv7();
3251
+ this.#syncAgentSessionId();
3252
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3253
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
3254
+ this.agent.appendOnlyContext?.invalidateForModelChange();
3255
+ return {
3256
+ previousSessionId,
3257
+ sessionId: this.sessionId,
3258
+ closedProviderSessions,
3259
+ };
3260
+ }
3261
+
3093
3262
  // =========================================================================
3094
3263
  // Read-only State Access
3095
3264
  // =========================================================================
@@ -3133,6 +3302,10 @@ export class AgentSession {
3133
3302
  return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
3134
3303
  }
3135
3304
 
3305
+ get isAborting(): boolean {
3306
+ return this.agent.isAborting;
3307
+ }
3308
+
3136
3309
  /** Wait until streaming and deferred recovery work are fully settled. */
3137
3310
  async waitForIdle(): Promise<void> {
3138
3311
  await this.agent.waitForIdle();
@@ -3299,6 +3472,17 @@ export class AgentSession {
3299
3472
  return this.#mcpDiscoveryEnabled;
3300
3473
  }
3301
3474
 
3475
+ /**
3476
+ * Flip MCP discovery on after deferred discovery learns the real tool count.
3477
+ * UI sessions resolve `tools.discoveryMode: "auto"` before MCP servers
3478
+ * connect, so a large MCP toolset discovered later must be able to upgrade
3479
+ * the session from the force-activate path to the discovery path. One-way:
3480
+ * discovery is never downgraded mid-session.
3481
+ */
3482
+ enableMCPDiscovery(): void {
3483
+ this.#mcpDiscoveryEnabled = true;
3484
+ }
3485
+
3302
3486
  getSelectedMCPToolNames(): string[] {
3303
3487
  if (!this.#mcpDiscoveryEnabled) {
3304
3488
  return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
@@ -3430,12 +3614,26 @@ export class AgentSession {
3430
3614
  * Wrap a tool with a permission-gate proxy when an ACP client is connected.
3431
3615
  * Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
3432
3616
  * the bridge exposes `requestPermission`. No-ops for all other cases.
3617
+ *
3618
+ * When the user has explicitly opted into `yolo` / auto-approve behavior (via
3619
+ * the SDK/CLI `autoApprove` flag or a configured `tools.approvalMode: yolo`),
3620
+ * skips the gate unless the per-tool policy explicitly requires a prompt or
3621
+ * deny. The schema default is also `yolo`, so an explicit configuration or
3622
+ * explicit session flag is required: default-config ACP sessions keep the
3623
+ * client-side permission gate.
3433
3624
  */
3434
3625
  #wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
3435
3626
  const bridge = this.#clientBridge;
3436
3627
  // Match the capability+method gating pattern used by read/write/bash.
3437
3628
  if (!bridge?.capabilities.requestPermission || !bridge.requestPermission) return tool;
3438
3629
  if (!PERMISSION_REQUIRED_TOOLS.has(tool.name)) return tool;
3630
+ // Skip the gate only on explicit yolo opt-in; honour per-tool policies
3631
+ // that require a prompt or deny (matching the normal approval wrapper).
3632
+ if (this.#isExplicitAutoApproveMode()) {
3633
+ const userPolicies = (this.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
3634
+ const toolPolicy = userPolicies[tool.name];
3635
+ if (!toolPolicy || toolPolicy === "allow") return tool;
3636
+ }
3439
3637
  return new Proxy(tool, {
3440
3638
  get: (target, prop) => {
3441
3639
  if (prop !== "execute") return Reflect.get(target, prop, target);
@@ -3523,6 +3721,13 @@ export class AgentSession {
3523
3721
  }) as T;
3524
3722
  }
3525
3723
 
3724
+ #isExplicitAutoApproveMode(): boolean {
3725
+ return (
3726
+ this.#autoApprove ||
3727
+ (this.settings.isConfigured("tools.approvalMode") && this.settings.get("tools.approvalMode") === "yolo")
3728
+ );
3729
+ }
3730
+
3526
3731
  async #applyActiveToolsByName(
3527
3732
  toolNames: string[],
3528
3733
  options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
@@ -3671,7 +3876,7 @@ export class AgentSession {
3671
3876
  }
3672
3877
 
3673
3878
  async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
3674
- const backend = resolveMemoryBackend(this.settings);
3879
+ const backend = await resolveMemoryBackend(this.settings);
3675
3880
  if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
3676
3881
 
3677
3882
  try {
@@ -3900,6 +4105,57 @@ export class AgentSession {
3900
4105
  return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
3901
4106
  }
3902
4107
 
4108
+ /**
4109
+ * Full-history transcript for TUI display: every path entry in
4110
+ * chronological order with compactions rendered inline at the point they
4111
+ * fired (instead of replacing prior history). Display-only — NEVER feed
4112
+ * the result to `agent.replaceMessages` or a provider.
4113
+ */
4114
+ buildTranscriptSessionContext(): SessionContext {
4115
+ return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
4116
+ }
4117
+
4118
+ #obfuscateForProvider<T>(value: T): T {
4119
+ if (!this.#obfuscator?.hasSecrets()) return value;
4120
+ return this.#obfuscator.obfuscateObject(value);
4121
+ }
4122
+
4123
+ #obfuscateTextForProvider(text: string | undefined): string | undefined {
4124
+ if (!text || !this.#obfuscator?.hasSecrets()) return text;
4125
+ return this.#obfuscator.obfuscate(text);
4126
+ }
4127
+
4128
+ #obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
4129
+ if (!this.#obfuscator?.hasSecrets()) return preparation;
4130
+ if (!preparation.previousSummary && !preparation.previousPreserveData) return preparation;
4131
+ return {
4132
+ ...preparation,
4133
+ previousSummary: preparation.previousSummary
4134
+ ? this.#obfuscator.obfuscate(preparation.previousSummary)
4135
+ : preparation.previousSummary,
4136
+ previousPreserveData: preparation.previousPreserveData
4137
+ ? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
4138
+ : preparation.previousPreserveData,
4139
+ };
4140
+ }
4141
+
4142
+ #deobfuscateFromProvider(text: string): string {
4143
+ if (!this.#obfuscator?.hasSecrets()) return text;
4144
+ return this.#obfuscator.deobfuscate(text);
4145
+ }
4146
+
4147
+ #deobfuscatedProviderTextReadyForDelta(text: string): string {
4148
+ const deobfuscated = this.#deobfuscateFromProvider(text);
4149
+ if (!this.#obfuscator?.hasSecrets()) return deobfuscated;
4150
+ const pendingPlaceholderStart = deobfuscated.match(/#[A-Z0-9]{0,4}$/);
4151
+ if (pendingPlaceholderStart?.index === undefined) return deobfuscated;
4152
+ return deobfuscated.slice(0, pendingPlaceholderStart.index);
4153
+ }
4154
+
4155
+ #convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
4156
+ return this.#obfuscateForProvider(convertToLlm(messages));
4157
+ }
4158
+
3903
4159
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
3904
4160
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
3905
4161
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -3994,7 +4250,7 @@ export class AgentSession {
3994
4250
 
3995
4251
  /** Current session ID */
3996
4252
  get sessionId(): string {
3997
- return this.#providerSessionId ?? this.sessionManager.getSessionId();
4253
+ return this.#activeProviderSessionId();
3998
4254
  }
3999
4255
  getEvalSessionId(): string | null {
4000
4256
  if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
@@ -4137,9 +4393,15 @@ export class AgentSession {
4137
4393
  return [...this.#customCommands, ...this.#mcpPromptCommands];
4138
4394
  }
4139
4395
 
4396
+ /** MCP prompt commands only, for command-list metadata. */
4397
+ get mcpPromptCommands(): ReadonlyArray<LoadedCustomCommand> {
4398
+ return this.#mcpPromptCommands;
4399
+ }
4400
+
4140
4401
  /** Update the MCP prompt commands list. Called when server prompts are (re)loaded. */
4141
4402
  setMCPPromptCommands(commands: LoadedCustomCommand[]): void {
4142
4403
  this.#mcpPromptCommands = commands;
4404
+ this.#notifyCommandMetadataChanged();
4143
4405
  }
4144
4406
 
4145
4407
  // =========================================================================
@@ -4231,6 +4493,73 @@ export class AgentSession {
4231
4493
  };
4232
4494
  }
4233
4495
 
4496
+ #normalizeImagesForModel(images: ImageContent[] | undefined): Promise<ImageContent[] | undefined> {
4497
+ return normalizeModelContextImages(images, { model: this.model });
4498
+ }
4499
+
4500
+ async #normalizeMessageContentImages(
4501
+ content: string | (TextContent | ImageContent)[],
4502
+ ): Promise<string | (TextContent | ImageContent)[]> {
4503
+ if (typeof content === "string") return content;
4504
+ const images = content.filter((part): part is ImageContent => part.type === "image");
4505
+ if (images.length === 0) return content;
4506
+ const normalizedImages = await this.#normalizeImagesForModel(images);
4507
+ if (!normalizedImages) return content;
4508
+ let imageIndex = 0;
4509
+ return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
4510
+ }
4511
+
4512
+ async #normalizeAgentMessageImages<T extends AgentMessage>(message: T): Promise<T> {
4513
+ if (!("content" in message)) return message;
4514
+ const content = message.content;
4515
+ if (typeof content !== "string" && !Array.isArray(content)) return message;
4516
+ const normalized = await this.#normalizeMessageContentImages(content as string | (TextContent | ImageContent)[]);
4517
+ if (normalized === content) return message;
4518
+ return { ...message, content: normalized } as T;
4519
+ }
4520
+
4521
+ #magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean {
4522
+ return this.settings.get("magicKeywords.enabled") && this.settings.get(`magicKeywords.${keyword}`);
4523
+ }
4524
+
4525
+ #createMagicKeywordNotices(text: string): CustomMessage[] {
4526
+ const timestamp = Date.now();
4527
+ const turnBudget = parseTurnBudget(text);
4528
+ this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4529
+ const keywordNotices: CustomMessage[] = [];
4530
+ if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(text)) {
4531
+ keywordNotices.push({
4532
+ role: "custom",
4533
+ customType: "ultrathink-notice",
4534
+ content: ULTRATHINK_NOTICE,
4535
+ display: false,
4536
+ attribution: "user",
4537
+ timestamp,
4538
+ });
4539
+ }
4540
+ if (this.#magicKeywordEnabled("orchestrate") && containsOrchestrate(text)) {
4541
+ keywordNotices.push({
4542
+ role: "custom",
4543
+ customType: "orchestrate-notice",
4544
+ content: ORCHESTRATE_NOTICE,
4545
+ display: false,
4546
+ attribution: "user",
4547
+ timestamp,
4548
+ });
4549
+ }
4550
+ if (this.#magicKeywordEnabled("workflow") && containsWorkflow(text)) {
4551
+ keywordNotices.push({
4552
+ role: "custom",
4553
+ customType: "workflow-notice",
4554
+ content: WORKFLOW_NOTICE,
4555
+ display: false,
4556
+ attribution: "user",
4557
+ timestamp,
4558
+ });
4559
+ }
4560
+ return keywordNotices;
4561
+ }
4562
+
4234
4563
  /**
4235
4564
  * Send a prompt to the agent.
4236
4565
  * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
@@ -4240,21 +4569,28 @@ export class AgentSession {
4240
4569
  * @throws Error if streaming and no streamingBehavior specified
4241
4570
  * @throws Error if no model selected or no API key available (when not streaming)
4242
4571
  */
4243
- async prompt(text: string, options?: PromptOptions): Promise<void> {
4572
+ /**
4573
+ * Returns `false` when the command was fully handled locally (extension or
4574
+ * custom-TS command consumed without calling the LLM). Returns `true` when
4575
+ * the prompt was forwarded to the agent — either directly or queued as a
4576
+ * steer/follow-up. Callers that render a UI or manage turn lifecycle (e.g.
4577
+ * the ACP agent) use this to know whether to expect an `agent_end` event.
4578
+ */
4579
+ async prompt(text: string, options?: PromptOptions): Promise<boolean> {
4244
4580
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
4245
4581
 
4246
4582
  // Handle extension commands first (execute immediately, even during streaming)
4247
4583
  if (expandPromptTemplates && text.startsWith("/")) {
4248
4584
  const handled = await this.#tryExecuteExtensionCommand(text);
4249
4585
  if (handled) {
4250
- return;
4586
+ return false;
4251
4587
  }
4252
4588
 
4253
4589
  // Try custom commands (TypeScript slash commands)
4254
4590
  const customResult = await this.#tryExecuteCustomCommand(text);
4255
4591
  if (customResult !== null) {
4256
4592
  if (customResult === "") {
4257
- return;
4593
+ return false;
4258
4594
  }
4259
4595
  text = customResult;
4260
4596
  }
@@ -4272,42 +4608,7 @@ export class AgentSession {
4272
4608
  // Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
4273
4609
  // user's message that steer this turn. User-authored prompts only — synthetic /
4274
4610
  // agent-initiated turns never trigger them.
4275
- const keywordNotices: CustomMessage[] = [];
4276
- if (!options?.synthetic) {
4277
- const timestamp = Date.now();
4278
- const turnBudget = parseTurnBudget(expandedText);
4279
- this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4280
- if (containsUltrathink(expandedText)) {
4281
- keywordNotices.push({
4282
- role: "custom",
4283
- customType: "ultrathink-notice",
4284
- content: ULTRATHINK_NOTICE,
4285
- display: false,
4286
- attribution: "user",
4287
- timestamp,
4288
- });
4289
- }
4290
- if (containsOrchestrate(expandedText)) {
4291
- keywordNotices.push({
4292
- role: "custom",
4293
- customType: "orchestrate-notice",
4294
- content: ORCHESTRATE_NOTICE,
4295
- display: false,
4296
- attribution: "user",
4297
- timestamp,
4298
- });
4299
- }
4300
- if (containsWorkflow(expandedText)) {
4301
- keywordNotices.push({
4302
- role: "custom",
4303
- customType: "workflow-notice",
4304
- content: WORKFLOW_NOTICE,
4305
- display: false,
4306
- attribution: "user",
4307
- timestamp,
4308
- });
4309
- }
4310
- }
4611
+ const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
4311
4612
 
4312
4613
  // If streaming, queue via steer() or followUp() based on option
4313
4614
  if (this.isStreaming) {
@@ -4315,25 +4616,28 @@ export class AgentSession {
4315
4616
  throw new AgentBusyError();
4316
4617
  }
4317
4618
  if (options.streamingBehavior === "followUp") {
4318
- await this.#queueFollowUp(expandedText, options?.images);
4619
+ await this.#queueUserMessage(expandedText, options?.images, "followUp");
4319
4620
  } else {
4320
- await this.#queueSteer(expandedText, options?.images);
4621
+ await this.#queueUserMessage(expandedText, options?.images, "steer");
4321
4622
  }
4322
4623
  // Steer/follow-up the keyword notices alongside the queued user message.
4323
4624
  for (const notice of keywordNotices) {
4324
4625
  await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4325
4626
  }
4326
- return;
4627
+ return true;
4327
4628
  }
4328
4629
 
4329
- // Skip eager todo prelude when the user has already queued a directive
4630
+ // Skip eager preludes when the user has already queued a directive
4330
4631
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4331
4632
  const eagerTodoPrelude =
4332
4633
  !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
4634
+ const eagerTaskPrelude =
4635
+ !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTaskPrelude(expandedText) : undefined;
4636
+ const normalizedImages = await this.#normalizeImagesForModel(options?.images);
4333
4637
 
4334
4638
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
4335
- if (options?.images) {
4336
- userContent.push(...options.images);
4639
+ if (normalizedImages) {
4640
+ userContent.push(...normalizedImages);
4337
4641
  }
4338
4642
 
4339
4643
  const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
@@ -4341,16 +4645,24 @@ export class AgentSession {
4341
4645
  ? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
4342
4646
  : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
4343
4647
 
4648
+ const preludeMessages: AgentMessage[] = [];
4344
4649
  if (eagerTodoPrelude) {
4345
- this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4346
- label: "eager-todo",
4347
- });
4650
+ if (eagerTodoPrelude.toolChoice) {
4651
+ this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
4652
+ label: "eager-todo",
4653
+ });
4654
+ }
4655
+ preludeMessages.push(eagerTodoPrelude.message);
4656
+ }
4657
+ if (eagerTaskPrelude) {
4658
+ preludeMessages.push(eagerTaskPrelude);
4348
4659
  }
4349
4660
 
4350
4661
  try {
4351
4662
  await this.#promptWithMessage(message, expandedText, {
4352
4663
  ...options,
4353
- prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
4664
+ images: normalizedImages,
4665
+ prependMessages: preludeMessages.length > 0 ? preludeMessages : undefined,
4354
4666
  appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4355
4667
  });
4356
4668
  } finally {
@@ -4361,11 +4673,12 @@ export class AgentSession {
4361
4673
  if (!options?.synthetic) {
4362
4674
  await this.#enforcePlanModeToolDecision();
4363
4675
  }
4676
+ return true;
4364
4677
  }
4365
4678
 
4366
4679
  async promptCustomMessage<T = unknown>(
4367
4680
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
4368
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
4681
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & { queueChipText?: string },
4369
4682
  ): Promise<void> {
4370
4683
  const textContent =
4371
4684
  typeof message.content === "string"
@@ -4375,11 +4688,27 @@ export class AgentSession {
4375
4688
  .map(content => content.text)
4376
4689
  .join("");
4377
4690
 
4691
+ let keywordNotices: CustomMessage[] = [];
4692
+ if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
4693
+ const details = message.details;
4694
+ let skillArgs = "";
4695
+ if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
4696
+ skillArgs = details.args;
4697
+ }
4698
+ keywordNotices = this.#createMagicKeywordNotices(skillArgs);
4699
+ }
4700
+
4378
4701
  if (this.isStreaming) {
4379
4702
  if (!options?.streamingBehavior) {
4380
4703
  throw new AgentBusyError();
4381
4704
  }
4382
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
4705
+ await this.sendCustomMessage(message, {
4706
+ deliverAs: options.streamingBehavior,
4707
+ queueChipText: options.queueChipText,
4708
+ });
4709
+ for (const notice of keywordNotices) {
4710
+ await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4711
+ }
4383
4712
  return;
4384
4713
  }
4385
4714
 
@@ -4393,7 +4722,10 @@ export class AgentSession {
4393
4722
  timestamp: Date.now(),
4394
4723
  };
4395
4724
 
4396
- await this.#promptWithMessage(customMessage, textContent, options);
4725
+ await this.#promptWithMessage(customMessage, textContent, {
4726
+ ...options,
4727
+ appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4728
+ });
4397
4729
  }
4398
4730
 
4399
4731
  async #promptWithMessage(
@@ -4411,7 +4743,7 @@ export class AgentSession {
4411
4743
  // Flush any pending bash messages before the new prompt
4412
4744
  this.#flushPendingBashMessages();
4413
4745
  this.#flushPendingPythonMessages();
4414
- this.#flushPendingBackgroundExchanges();
4746
+ this.#flushPendingIrcAsides();
4415
4747
 
4416
4748
  // Reset todo reminder count on new user prompt
4417
4749
  this.#todoReminderCount = 0;
@@ -4492,7 +4824,9 @@ export class AgentSession {
4492
4824
  useHashLines: resolveFileDisplayMode(this).hashLines,
4493
4825
  snapshotStore: getFileSnapshotStore(this),
4494
4826
  });
4495
- messages.push(...fileMentionMessages);
4827
+ for (const fileMentionMessage of fileMentionMessages) {
4828
+ messages.push(await this.#normalizeAgentMessageImages(fileMentionMessage));
4829
+ }
4496
4830
  }
4497
4831
 
4498
4832
  const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
@@ -4508,15 +4842,18 @@ export class AgentSession {
4508
4842
  const promptAttribution: "user" | "agent" | undefined =
4509
4843
  "attribution" in message ? message.attribution : undefined;
4510
4844
  for (const msg of result.messages) {
4511
- messages.push({
4512
- role: "custom",
4513
- customType: msg.customType,
4514
- content: msg.content,
4515
- display: msg.display,
4516
- details: msg.details,
4517
- attribution: msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
4518
- timestamp: Date.now(),
4519
- });
4845
+ messages.push(
4846
+ await this.#normalizeAgentMessageImages({
4847
+ role: "custom",
4848
+ customType: msg.customType,
4849
+ content: msg.content,
4850
+ display: msg.display,
4851
+ details: msg.details,
4852
+ attribution:
4853
+ msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
4854
+ timestamp: Date.now(),
4855
+ }),
4856
+ );
4520
4857
  }
4521
4858
  }
4522
4859
 
@@ -4553,7 +4890,7 @@ export class AgentSession {
4553
4890
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
4554
4891
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
4555
4892
  if (!options?.skipPostPromptRecoveryWait) {
4556
- await this.#waitForPostPromptRecovery();
4893
+ await this.#waitForPostPromptRecovery(generation);
4557
4894
  }
4558
4895
  } finally {
4559
4896
  this.#endInFlight();
@@ -4603,6 +4940,7 @@ export class AgentSession {
4603
4940
  sessionManager: this.sessionManager,
4604
4941
  modelRegistry: this.#modelRegistry,
4605
4942
  model: this.model ?? undefined,
4943
+ models: createExtensionModelQuery(this.#modelRegistry, this.settings, () => this.model ?? undefined),
4606
4944
  isIdle: () => !this.isStreaming,
4607
4945
  abort: () => {
4608
4946
  void this.abort();
@@ -4705,7 +5043,7 @@ export class AgentSession {
4705
5043
  }
4706
5044
 
4707
5045
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
4708
- await this.#queueSteer(expandedText, images);
5046
+ await this.#queueUserMessage(expandedText, images, "steer");
4709
5047
  }
4710
5048
 
4711
5049
  /**
@@ -4717,68 +5055,74 @@ export class AgentSession {
4717
5055
  }
4718
5056
 
4719
5057
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
4720
- await this.#queueFollowUp(expandedText, images);
5058
+ await this.#queueUserMessage(expandedText, images, "followUp");
4721
5059
  }
4722
5060
 
4723
- /**
4724
- * Internal: Queue a steering message (already expanded, no extension command check).
4725
- */
4726
- async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
4727
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
4728
- this.#steeringMessages.push({ text: displayText });
5061
+ async #queueUserMessage(
5062
+ text: string,
5063
+ images: ImageContent[] | undefined,
5064
+ mode: "steer" | "followUp",
5065
+ ): Promise<void> {
5066
+ const normalizedImages = await this.#normalizeImagesForModel(images);
4729
5067
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
4730
- if (images && images.length > 0) {
4731
- content.push(...images);
5068
+ if (normalizedImages?.length) {
5069
+ content.push(...normalizedImages);
5070
+ }
5071
+ if (mode === "followUp") {
5072
+ this.agent.followUp({
5073
+ role: "user",
5074
+ content,
5075
+ attribution: "user",
5076
+ timestamp: Date.now(),
5077
+ });
5078
+ } else {
5079
+ this.agent.steer({
5080
+ role: "user",
5081
+ content,
5082
+ steering: true,
5083
+ attribution: "user",
5084
+ timestamp: Date.now(),
5085
+ });
4732
5086
  }
4733
- this.agent.steer({
4734
- role: "user",
4735
- content,
4736
- steering: true,
4737
- attribution: "user",
4738
- timestamp: Date.now(),
4739
- });
5087
+ this.#scheduleIdleQueueDrain();
4740
5088
  }
4741
5089
 
4742
- /**
4743
- * Internal: Queue a follow-up message (already expanded, no extension command check).
4744
- */
4745
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
4746
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
4747
- this.#followUpMessages.push({ text: displayText });
4748
- const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
4749
- if (images && images.length > 0) {
4750
- content.push(...images);
5090
+ #scheduleIdleQueueDrain(): void {
5091
+ this.#scheduleQueuedMessageDrain();
5092
+ }
5093
+
5094
+ #scheduleQueuedMessageDrain(): void {
5095
+ if (this.#queuedMessageDrainScheduled || !this.#canAutoContinueForFollowUp() || !this.agent.hasQueuedMessages()) {
5096
+ return;
4751
5097
  }
4752
- this.agent.followUp({
4753
- role: "user",
4754
- content,
4755
- attribution: "user",
4756
- timestamp: Date.now(),
5098
+ this.#queuedMessageDrainScheduled = true;
5099
+ this.#scheduleAgentContinue({
5100
+ shouldContinue: () => {
5101
+ this.#queuedMessageDrainScheduled = false;
5102
+ return this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages();
5103
+ },
5104
+ onSkip: () => {
5105
+ this.#queuedMessageDrainScheduled = false;
5106
+ },
5107
+ onError: () => {
5108
+ this.#queuedMessageDrainScheduled = false;
5109
+ },
4757
5110
  });
4758
- // When fully idle AND the session is in a resumable assistant-ended state,
4759
- // schedule an immediate continue so the queued follow-up is delivered
4760
- // without waiting for the next user turn. We gate on isStreaming (model
4761
- // actively producing), isRetrying (auto-retry backoff is sleeping between
4762
- // attempts, #retryPromise set), and the last message being assistant —
4763
- // agent.continue() only dequeues follow-ups from an assistant-ended state;
4764
- // resuming from user/toolResult state runs an extra model call on the
4765
- // stale prompt before draining the queue.
4766
- if (this.#canAutoContinueForFollowUp()) {
4767
- this.#scheduleAgentContinue({
4768
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
4769
- });
4770
- }
4771
5111
  }
4772
5112
 
4773
5113
  /**
4774
- * Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
5114
+ * Gate for idle-path queued-message auto-continue. See `#scheduleIdleQueueDrain` for rationale.
4775
5115
  */
4776
5116
  #canAutoContinueForFollowUp(): boolean {
4777
5117
  if (this.isStreaming) return false;
4778
5118
  if (this.isRetrying) return false;
4779
5119
  const messages = this.agent.state.messages;
4780
5120
  const last = messages[messages.length - 1];
4781
- return last?.role === "assistant";
5121
+ // A user interrupt during tool execution can leave the transcript ending
5122
+ // with the emitted tool result, not the aborted assistant message. Continuing
5123
+ // from that state is still resumable: Agent.continue() first polls queued
5124
+ // steering before making the next model call.
5125
+ return last?.role === "assistant" || last?.role === "toolResult";
4782
5126
  }
4783
5127
 
4784
5128
  queueDeferredMessage(message: CustomMessage): void {
@@ -4877,71 +5221,87 @@ export class AgentSession {
4877
5221
  * - Streaming: queue as steer/follow-up or store for next turn
4878
5222
  * - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it
4879
5223
  * - Not streaming + no trigger: appends to state/session, no turn
5224
+ *
5225
+ * @returns true iff this call synchronously started a new turn (awaited
5226
+ * `agent.prompt`); false when the message was queued/appended without a turn.
4880
5227
  */
4881
5228
  async sendCustomMessage<T = unknown>(
4882
5229
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
4883
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
4884
- ): Promise<void> {
5230
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
5231
+ ): Promise<boolean> {
5232
+ const details =
5233
+ options?.queueChipText && options.deliverAs !== "nextTurn"
5234
+ ? ({
5235
+ ...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
5236
+ string,
5237
+ unknown
5238
+ >),
5239
+ __queueChipText: options.queueChipText,
5240
+ } as T)
5241
+ : message.details;
4885
5242
  const appMessage: CustomMessage<T> = {
4886
5243
  role: "custom",
4887
5244
  customType: message.customType,
4888
5245
  content: message.content,
4889
5246
  display: message.display,
4890
- details: message.details,
5247
+ details,
4891
5248
  attribution: message.attribution ?? "agent",
4892
5249
  timestamp: Date.now(),
4893
5250
  };
5251
+ const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
4894
5252
  if (this.isStreaming) {
4895
5253
  if (options?.deliverAs === "nextTurn") {
4896
- this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false);
4897
- return;
5254
+ this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
5255
+ return false;
4898
5256
  }
4899
5257
 
4900
5258
  if (options?.deliverAs === "followUp") {
4901
- this.agent.followUp(appMessage);
5259
+ this.agent.followUp(normalizedAppMessage);
4902
5260
  } else {
4903
- this.agent.steer(appMessage);
5261
+ this.agent.steer(normalizedAppMessage);
4904
5262
  }
4905
- return;
5263
+ this.#scheduleIdleQueueDrain();
5264
+ return false;
4906
5265
  }
4907
5266
 
4908
5267
  if (options?.deliverAs === "nextTurn") {
4909
5268
  if (options?.triggerTurn) {
4910
5269
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
4911
- this.#queueHiddenNextTurnMessage(appMessage, false);
4912
- return;
5270
+ this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5271
+ return false;
4913
5272
  }
4914
- await this.agent.prompt(appMessage);
4915
- return;
5273
+ await this.agent.prompt(normalizedAppMessage);
5274
+ return true;
4916
5275
  }
4917
- this.agent.appendMessage(appMessage);
5276
+ this.agent.appendMessage(normalizedAppMessage);
4918
5277
  this.sessionManager.appendCustomMessageEntry(
4919
- message.customType,
4920
- message.content,
5278
+ normalizedAppMessage.customType,
5279
+ normalizedAppMessage.content,
4921
5280
  message.display,
4922
5281
  message.details,
4923
5282
  message.attribution ?? "agent",
4924
5283
  );
4925
- return;
5284
+ return false;
4926
5285
  }
4927
5286
 
4928
5287
  if (options?.triggerTurn) {
4929
5288
  if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
4930
- this.#queueHiddenNextTurnMessage(appMessage, false);
4931
- return;
5289
+ this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
5290
+ return false;
4932
5291
  }
4933
- await this.agent.prompt(appMessage);
4934
- return;
5292
+ await this.agent.prompt(normalizedAppMessage);
5293
+ return true;
4935
5294
  }
4936
5295
 
4937
- this.agent.appendMessage(appMessage);
5296
+ this.agent.appendMessage(normalizedAppMessage);
4938
5297
  this.sessionManager.appendCustomMessageEntry(
4939
- message.customType,
4940
- message.content,
5298
+ normalizedAppMessage.customType,
5299
+ normalizedAppMessage.content,
4941
5300
  message.display,
4942
5301
  message.details,
4943
5302
  message.attribution ?? "agent",
4944
5303
  );
5304
+ return false;
4945
5305
  }
4946
5306
 
4947
5307
  /**
@@ -4976,11 +5336,11 @@ export class AgentSession {
4976
5336
  }
4977
5337
 
4978
5338
  if (options?.deliverAs === "followUp") {
4979
- await this.#queueFollowUp(text, images);
5339
+ await this.#queueUserMessage(text, images, "followUp");
4980
5340
  return;
4981
5341
  }
4982
5342
  if (options?.deliverAs === "steer") {
4983
- await this.#queueSteer(text, images);
5343
+ await this.#queueUserMessage(text, images, "steer");
4984
5344
  return;
4985
5345
  }
4986
5346
 
@@ -4991,55 +5351,37 @@ export class AgentSession {
4991
5351
  });
4992
5352
  }
4993
5353
 
4994
- /**
4995
- * Clear queued messages and return them.
4996
- * Useful for restoring to editor when user aborts.
4997
- */
4998
- clearQueue(): { steering: string[]; followUp: string[] } {
4999
- const steering = this.#steeringMessages.map(e => e.text);
5000
- const followUp = this.#followUpMessages.map(e => e.text);
5001
- this.#steeringMessages = [];
5002
- this.#followUpMessages = [];
5354
+ /** Clear queued messages and return them (text plus any attached images). */
5355
+ clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
5356
+ const steering = this.agent.peekSteeringQueue().map(toRestoredQueuedMessage);
5357
+ const followUp = this.agent.peekFollowUpQueue().map(toRestoredQueuedMessage);
5003
5358
  this.agent.clearAllQueues();
5004
5359
  return { steering, followUp };
5005
5360
  }
5006
5361
 
5007
- /** Number of pending messages (includes steering, follow-up, and next-turn messages) */
5362
+ /** Number of pending displayable messages (includes steering, follow-up, and next-turn messages) */
5008
5363
  get queuedMessageCount(): number {
5009
- return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
5364
+ return (
5365
+ this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
5366
+ this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).length +
5367
+ this.#pendingNextTurnMessages.length
5368
+ );
5010
5369
  }
5011
5370
 
5012
- /** Get pending messages (read-only). Returns the public text-only view;
5013
- * internal `{text, tag?}` records are mapped to `.text` so callers
5014
- * (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
5015
- * the unchanged historical shape. */
5016
5371
  getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
5017
5372
  return {
5018
- steering: this.#steeringMessages.map(e => e.text),
5019
- followUp: this.#followUpMessages.map(e => e.text),
5373
+ steering: this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5374
+ followUp: this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5020
5375
  };
5021
5376
  }
5022
5377
 
5023
5378
  /**
5024
5379
  * Pop the last queued message (steering first, then follow-up).
5025
5380
  * Used by dequeue keybinding to restore messages to editor one at a time.
5026
- * Returns the popped entry's `.text`; the tag (if any) dies with the
5027
- * record — no orphan state can outlive the queue entry.
5028
5381
  */
5029
- popLastQueuedMessage(): string | undefined {
5030
- // Pop from steering first (LIFO)
5031
- if (this.#steeringMessages.length > 0) {
5032
- const entry = this.#steeringMessages.pop();
5033
- this.agent.popLastSteer();
5034
- return entry?.text;
5035
- }
5036
- // Then from follow-up
5037
- if (this.#followUpMessages.length > 0) {
5038
- const entry = this.#followUpMessages.pop();
5039
- this.agent.popLastFollowUp();
5040
- return entry?.text;
5041
- }
5042
- return undefined;
5382
+ popLastQueuedMessage(): RestoredQueuedMessage | undefined {
5383
+ const message = this.agent.popLastSteer() ?? this.agent.popLastFollowUp();
5384
+ return message ? toRestoredQueuedMessage(message) : undefined;
5043
5385
  }
5044
5386
 
5045
5387
  get skillsSettings(): SkillsSettings | undefined {
@@ -5077,11 +5419,7 @@ export class AgentSession {
5077
5419
  #cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
5078
5420
  return phases.map(phase => ({
5079
5421
  name: phase.name,
5080
- tasks: phase.tasks.map(task => {
5081
- const out: TodoItem = { content: task.content, status: task.status };
5082
- if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
5083
- return out;
5084
- }),
5422
+ tasks: phase.tasks.map(task => ({ content: task.content, status: task.status })),
5085
5423
  }));
5086
5424
  }
5087
5425
 
@@ -5093,30 +5431,44 @@ export class AgentSession {
5093
5431
 
5094
5432
  /**
5095
5433
  * Abort current operation and wait for agent to become idle.
5434
+ *
5435
+ * `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
5436
+ * and surfaces verbatim on the aborted assistant message's `errorMessage`, so
5437
+ * the transcript can distinguish a deliberate user interrupt from an opaque
5438
+ * abort. Omit it for internal/lifecycle aborts.
5096
5439
  */
5097
- async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
5098
- this.abortRetry();
5099
- this.#promptGeneration++;
5100
- this.#scheduledHiddenNextTurnGeneration = undefined;
5101
- this.abortCompaction();
5102
- this.abortHandoff();
5103
- this.abortBash();
5104
- this.abortEval();
5105
- const postPromptDrain = this.#cancelPostPromptTasks();
5106
- this.agent.abort();
5107
- await postPromptDrain;
5108
- await this.agent.waitForIdle();
5109
- await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5110
- // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5111
- // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5112
- // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5113
- this.#resetInFlight();
5114
- // Safety net: if the agent loop aborted without producing an assistant
5115
- // message (e.g. failed before the first stream), the in-flight yield was
5116
- // never resolved or rejected by the normal message_end path. Reject it now
5117
- // so any requeue callback still fires and the queue stays consistent.
5118
- if (this.#toolChoiceQueue.hasInFlight) {
5119
- this.#toolChoiceQueue.reject("aborted");
5440
+ async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5441
+ // Session switch/compact paths disconnect first; explicit aborts should
5442
+ // leave any queued steer/follow-up visible for the user rather than
5443
+ // auto-starting a fresh turn during cleanup.
5444
+ this.#abortInProgress = true;
5445
+ try {
5446
+ this.abortRetry();
5447
+ this.#promptGeneration++;
5448
+ this.#scheduledHiddenNextTurnGeneration = undefined;
5449
+ this.abortCompaction();
5450
+ this.abortHandoff();
5451
+ this.abortBash();
5452
+ this.abortEval();
5453
+ const postPromptDrain = this.#cancelPostPromptTasks();
5454
+ this.agent.abort(options?.reason);
5455
+ await postPromptDrain;
5456
+ await this.agent.waitForIdle();
5457
+ await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
5458
+ // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
5459
+ // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5460
+ // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5461
+ this.#resetInFlight();
5462
+ // Safety net: if the agent loop aborted without producing an assistant
5463
+ // message (e.g. failed before the first stream), the in-flight yield was
5464
+ // never resolved or rejected by the normal message_end path. Reject it now
5465
+ // so any requeue callback still fires and the queue stays consistent.
5466
+ if (this.#toolChoiceQueue.hasInFlight) {
5467
+ this.#toolChoiceQueue.reject("aborted");
5468
+ }
5469
+ } finally {
5470
+ this.#abortInProgress = false;
5471
+ this.#drainStrandedQueuedMessages();
5120
5472
  }
5121
5473
  }
5122
5474
 
@@ -5164,13 +5516,12 @@ export class AgentSession {
5164
5516
  }
5165
5517
  await this.sessionManager.newSession(options);
5166
5518
  this.setTodoPhases([]);
5519
+ this.#freshProviderSessionId = undefined;
5167
5520
  this.#syncAgentSessionId();
5168
5521
  this.#rekeyHindsightMemoryForCurrentSessionId();
5169
5522
  this.#rekeyMnemopiMemoryForCurrentSessionId();
5170
5523
  this.#resetHindsightConversationTrackingIfHindsight();
5171
5524
  this.#resetMnemopiConversationTrackingIfMnemopi();
5172
- this.#steeringMessages = [];
5173
- this.#followUpMessages = [];
5174
5525
  this.#pendingNextTurnMessages = [];
5175
5526
  this.#scheduledHiddenNextTurnGeneration = undefined;
5176
5527
 
@@ -5261,6 +5612,7 @@ export class AgentSession {
5261
5612
  }
5262
5613
 
5263
5614
  // Update agent session ID
5615
+ this.#freshProviderSessionId = undefined;
5264
5616
  this.#syncAgentSessionId();
5265
5617
  this.#rekeyHindsightMemoryForCurrentSessionId();
5266
5618
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -5284,7 +5636,10 @@ export class AgentSession {
5284
5636
 
5285
5637
  /**
5286
5638
  * Set model directly.
5287
- * Validates API key and saves to the active session. Persists settings only when requested.
5639
+ * Validates that a credential source is configured (synchronously, without
5640
+ * refreshing OAuth or running command-backed key programs) and saves to the
5641
+ * active session. Persists settings only when requested. The concrete key is
5642
+ * resolved lazily per request, so switching never blocks the event loop.
5288
5643
  * @throws Error if no API key available for the model
5289
5644
  */
5290
5645
  async setModel(
@@ -5293,8 +5648,7 @@ export class AgentSession {
5293
5648
  options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5294
5649
  ): Promise<void> {
5295
5650
  const previousEditMode = this.#resolveActiveEditMode();
5296
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5297
- if (!apiKey) {
5651
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5298
5652
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5299
5653
  }
5300
5654
 
@@ -5317,7 +5671,9 @@ export class AgentSession {
5317
5671
 
5318
5672
  /**
5319
5673
  * Set model temporarily (for this session only).
5320
- * Validates API key, saves to session log but NOT to settings.
5674
+ * Validates that a credential source is configured (synchronously, without
5675
+ * refreshing OAuth or running command-backed key programs), saves to session
5676
+ * log but NOT to settings.
5321
5677
  * @throws Error if no API key available for the model
5322
5678
  */
5323
5679
  async setModelTemporary(
@@ -5326,8 +5682,7 @@ export class AgentSession {
5326
5682
  options?: { ephemeral?: boolean },
5327
5683
  ): Promise<void> {
5328
5684
  const previousEditMode = this.#resolveActiveEditMode();
5329
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5330
- if (!apiKey) {
5685
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5331
5686
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5332
5687
  }
5333
5688
 
@@ -5378,7 +5733,7 @@ export class AgentSession {
5378
5733
 
5379
5734
  const currentModel = this.model;
5380
5735
  if (!currentModel) return undefined;
5381
- const matchPreferences = { usageOrder: this.settings.getStorage()?.getModelUsageOrder() };
5736
+ const matchPreferences = getModelMatchPreferences(this.settings);
5382
5737
  const models: ResolvedRoleModel[] = [];
5383
5738
 
5384
5739
  for (const role of roleOrder) {
@@ -5525,16 +5880,25 @@ export class AgentSession {
5525
5880
  }
5526
5881
 
5527
5882
  /**
5528
- * Get all available models with valid API keys.
5883
+ * Get all available models with valid API keys, filtered by `enabledModels` when configured.
5884
+ * See {@link filterAvailableModelsByEnabledPatterns} for supported pattern forms and limitations.
5529
5885
  */
5530
5886
  getAvailableModels(): Model[] {
5531
- return this.#modelRegistry.getAvailable();
5887
+ const all = this.#modelRegistry.getAvailable();
5888
+ const patterns = this.settings.get("enabledModels");
5889
+ if (!patterns || patterns.length === 0) return all;
5890
+ return filterAvailableModelsByEnabledPatterns(all, patterns, this.#modelRegistry);
5532
5891
  }
5533
5892
 
5534
5893
  // =========================================================================
5535
5894
  // Thinking Level Management
5536
5895
  // =========================================================================
5537
5896
 
5897
+ #applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void {
5898
+ this.agent.setThinkingLevel(toReasoningEffort(level));
5899
+ this.agent.setDisableReasoning(shouldDisableReasoning(level));
5900
+ }
5901
+
5538
5902
  /**
5539
5903
  * Set the thinking level. `auto` enables per-turn classification; the selector
5540
5904
  * itself is never written to the session log, but resolved concrete levels are
@@ -5548,7 +5912,7 @@ export class AgentSession {
5548
5912
  this.#autoThinking = true;
5549
5913
  this.#autoResolvedLevel = undefined;
5550
5914
  this.#thinkingLevel = provisional;
5551
- this.agent.setThinkingLevel(toReasoningEffort(provisional));
5915
+ this.#applyThinkingLevelToAgent(provisional);
5552
5916
  if (persist) {
5553
5917
  this.settings.set("defaultThinkingLevel", AUTO_THINKING);
5554
5918
  }
@@ -5564,7 +5928,7 @@ export class AgentSession {
5564
5928
  const isChanging = effectiveLevel !== this.#thinkingLevel;
5565
5929
 
5566
5930
  this.#thinkingLevel = effectiveLevel;
5567
- this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
5931
+ this.#applyThinkingLevelToAgent(effectiveLevel);
5568
5932
 
5569
5933
  if (isChanging) {
5570
5934
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
@@ -5621,7 +5985,7 @@ export class AgentSession {
5621
5985
  if (!model?.reasoning) return;
5622
5986
 
5623
5987
  let resolved: Effort | undefined;
5624
- if (containsUltrathink(promptText)) {
5988
+ if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
5625
5989
  // The user explicitly asked for maximum thinking; bypass the classifier
5626
5990
  // and jump straight to the highest auto-supported level for this model.
5627
5991
  resolved = clampAutoThinkingEffort(model, Effort.XHigh);
@@ -5654,7 +6018,7 @@ export class AgentSession {
5654
6018
  const shouldPersistResolution = this.#autoResolvedLevel !== effort;
5655
6019
  this.#autoResolvedLevel = effort;
5656
6020
  this.#thinkingLevel = effort;
5657
- this.agent.setThinkingLevel(toReasoningEffort(effort));
6021
+ this.#applyThinkingLevelToAgent(effort);
5658
6022
  if (shouldPersistResolution) {
5659
6023
  this.sessionManager.appendThinkingLevelChange(effort);
5660
6024
  }
@@ -5710,7 +6074,12 @@ export class AgentSession {
5710
6074
  // Already on under any scope — keep the user's scoped value.
5711
6075
  return;
5712
6076
  }
5713
- this.setServiceTier(enabled ? "priority" : undefined);
6077
+ if (!enabled) {
6078
+ this.setServiceTier(undefined);
6079
+ return;
6080
+ }
6081
+ const scope = this.settings.get("fastModeScope");
6082
+ this.setServiceTier(scope === "openai" ? "openai-only" : scope === "claude" ? "claude-only" : "priority");
5714
6083
  }
5715
6084
 
5716
6085
  toggleFastMode(): boolean {
@@ -5775,7 +6144,45 @@ export class AgentSession {
5775
6144
 
5776
6145
  async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
5777
6146
  const branchEntries = this.sessionManager.getBranch();
5778
- const result = pruneToolOutputs(branchEntries, this.#withPlanProtection(DEFAULT_PRUNE_CONFIG));
6147
+ const result = pruneToolOutputs(
6148
+ branchEntries,
6149
+ this.#withPlanProtection({
6150
+ ...DEFAULT_PRUNE_CONFIG,
6151
+ pruneUseless: this.settings.getGroup("compaction").dropUseless,
6152
+ }),
6153
+ );
6154
+ if (result.prunedCount === 0) {
6155
+ return undefined;
6156
+ }
6157
+
6158
+ await this.sessionManager.rewriteEntries();
6159
+ const sessionContext = this.buildDisplaySessionContext();
6160
+ this.agent.replaceMessages(sessionContext.messages);
6161
+ this.#syncTodoPhasesFromBranch();
6162
+ this.#closeCodexProviderSessionsForHistoryRewrite();
6163
+ return result;
6164
+ }
6165
+
6166
+ /**
6167
+ * Per-turn stale-result pass: prune older `read` results that a newer read
6168
+ * of the same file has made stale, plus results their tool flagged
6169
+ * contextually useless. Cache-aware (only fires when the suffix after a
6170
+ * candidate is small or the session has been idle long enough that the
6171
+ * provider prompt cache is cold), so it is cheap to run every turn. Gated
6172
+ * on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
6173
+ */
6174
+ async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
6175
+ const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
6176
+ if (!supersedeReads && !dropUseless) return undefined;
6177
+ const branchEntries = this.sessionManager.getBranch();
6178
+ const result = pruneSupersededToolResults(
6179
+ branchEntries,
6180
+ this.#withPlanProtection({
6181
+ supersedeKey: supersedeReads ? readToolSupersedeKey : undefined,
6182
+ pruneUseless: dropUseless,
6183
+ protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
6184
+ }),
6185
+ );
5779
6186
  if (result.prunedCount === 0) {
5780
6187
  return undefined;
5781
6188
  }
@@ -5977,6 +6384,20 @@ export class AgentSession {
5977
6384
 
5978
6385
  const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
5979
6386
 
6387
+ // Strategy honored on manual /compact too. Custom instructions imply a
6388
+ // directed LLM summary; a text-only model cannot read the frames back —
6389
+ // both take the summarizer path (the latter loudly).
6390
+ const wantsSnapcompact =
6391
+ compactionPrep.kind !== "fromHook" && compactionSettings.strategy === "snapcompact" && !customInstructions;
6392
+ const snapcompactReady = wantsSnapcompact && this.model.input.includes("image");
6393
+ if (wantsSnapcompact && !snapcompactReady) {
6394
+ this.emitNotice(
6395
+ "warning",
6396
+ `snapcompact needs a vision-capable model (${this.model.id} is text-only) — using an LLM summary instead`,
6397
+ "compaction",
6398
+ );
6399
+ }
6400
+
5980
6401
  let summary: string;
5981
6402
  let shortSummary: string | undefined;
5982
6403
  let firstKeptEntryId: string;
@@ -5990,6 +6411,22 @@ export class AgentSession {
5990
6411
  tokensBefore = compactionPrep.tokensBefore;
5991
6412
  details = compactionPrep.details;
5992
6413
  preserveData = compactionPrep.preserveData;
6414
+ } else if (snapcompactReady) {
6415
+ const snapcompactResult = await snapcompact.compact(preparation, {
6416
+ convertToLlm,
6417
+ model: this.model,
6418
+ thinkingLevel: this.thinkingLevel,
6419
+ shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
6420
+ // Providers with hard image caps (OpenRouter: 8) silently drop
6421
+ // frames past the cap — keep the archive within budget.
6422
+ maxFrames: snapcompact.providerFrameBudget(this.model.provider),
6423
+ });
6424
+ summary = snapcompactResult.summary;
6425
+ shortSummary = snapcompactResult.shortSummary;
6426
+ firstKeptEntryId = snapcompactResult.firstKeptEntryId;
6427
+ tokensBefore = snapcompactResult.tokensBefore;
6428
+ details = snapcompactResult.details;
6429
+ preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
5993
6430
  } else {
5994
6431
  // Generate compaction result. Only convert known abort-shaped
5995
6432
  // rejections (AbortError raised while the abort signal is set,
@@ -6008,10 +6445,10 @@ export class AgentSession {
6008
6445
  customInstructions,
6009
6446
  compactionAbortController.signal,
6010
6447
  {
6011
- promptOverride: compactionPrep.hookPrompt,
6012
- extraContext: compactionPrep.hookContext,
6013
- remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
6014
- convertToLlm,
6448
+ promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
6449
+ extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
6450
+ remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
6451
+ convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
6015
6452
  },
6016
6453
  );
6017
6454
  summary = result.summary;
@@ -6097,7 +6534,7 @@ export class AgentSession {
6097
6534
  messagesToSummarize: AgentMessage[];
6098
6535
  turnPrefixMessages: AgentMessage[];
6099
6536
  }): Promise<string | undefined> {
6100
- const backend = resolveMemoryBackend(this.settings);
6537
+ const backend = await resolveMemoryBackend(this.settings);
6101
6538
  if (!backend.preCompactionContext) return undefined;
6102
6539
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
6103
6540
  try {
@@ -6194,15 +6631,15 @@ export class AgentSession {
6194
6631
  throw new Error(`No API key for ${model.provider}`);
6195
6632
  }
6196
6633
 
6197
- const handoffText = await generateHandoff(
6634
+ const rawHandoffText = await generateHandoff(
6198
6635
  this.agent.state.messages,
6199
6636
  model,
6200
- apiKey,
6637
+ this.#modelRegistry.resolver(model, this.sessionId),
6201
6638
  {
6202
- systemPrompt: this.#baseSystemPrompt,
6203
- tools: this.agent.state.tools,
6204
- customInstructions,
6205
- convertToLlm,
6639
+ systemPrompt: this.#obfuscateForProvider(this.#baseSystemPrompt),
6640
+ tools: obfuscateProviderTools(this.#obfuscator, this.agent.state.tools),
6641
+ customInstructions: this.#obfuscateTextForProvider(customInstructions),
6642
+ convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
6206
6643
  initiatorOverride: "agent",
6207
6644
  metadata: this.agent.metadataForProvider(model.provider),
6208
6645
  telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
@@ -6214,6 +6651,7 @@ export class AgentSession {
6214
6651
  },
6215
6652
  handoffSignal,
6216
6653
  );
6654
+ const handoffText = this.#deobfuscateFromProvider(rawHandoffText);
6217
6655
 
6218
6656
  if (handoffSignal.aborted) {
6219
6657
  throw new Error("Handoff cancelled");
@@ -6228,13 +6666,12 @@ export class AgentSession {
6228
6666
  this.#cancelOwnAsyncJobs();
6229
6667
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
6230
6668
  this.agent.reset();
6669
+ this.#freshProviderSessionId = undefined;
6231
6670
  this.#syncAgentSessionId();
6232
6671
  this.#rekeyHindsightMemoryForCurrentSessionId();
6233
6672
  this.#rekeyMnemopiMemoryForCurrentSessionId();
6234
6673
  this.#resetHindsightConversationTrackingIfHindsight();
6235
6674
  this.#resetMnemopiConversationTrackingIfMnemopi();
6236
- this.#steeringMessages = [];
6237
- this.#followUpMessages = [];
6238
6675
  this.#pendingNextTurnMessages = [];
6239
6676
  this.#scheduledHiddenNextTurnGeneration = undefined;
6240
6677
  this.#todoReminderCount = 0;
@@ -6407,7 +6844,10 @@ export class AgentSession {
6407
6844
  model: `${assistantMessage.provider}/${assistantMessage.model}`,
6408
6845
  strategy: incompleteCompactionSettings.strategy,
6409
6846
  });
6410
- await this.#runAutoCompaction("incomplete", true, false, allowDefer, { autoContinue });
6847
+ await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
6848
+ autoContinue,
6849
+ triggerContextTokens: calculateContextTokens(assistantMessage.usage),
6850
+ });
6411
6851
  } else {
6412
6852
  // Neither promotion nor compaction is available — surface the dead-end so
6413
6853
  // the user understands why the turn yielded with nothing.
@@ -6418,6 +6858,11 @@ export class AgentSession {
6418
6858
  return false;
6419
6859
  }
6420
6860
 
6861
+ // Stale-result pass runs every turn, before any threshold gating: it is
6862
+ // cheap (bails when no candidate) and independent of the compaction
6863
+ // setting.
6864
+ const supersedeResult = await this.#pruneStaleToolResults();
6865
+
6421
6866
  const compactionSettings = this.settings.getGroup("compaction");
6422
6867
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
6423
6868
 
@@ -6426,6 +6871,9 @@ export class AgentSession {
6426
6871
  if (assistantMessage.stopReason === "error") return false;
6427
6872
  const pruneResult = await this.#pruneToolOutputs();
6428
6873
  let contextTokens = calculateContextTokens(assistantMessage.usage);
6874
+ if (supersedeResult) {
6875
+ contextTokens = Math.max(0, contextTokens - supersedeResult.tokensSaved);
6876
+ }
6429
6877
  if (pruneResult) {
6430
6878
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6431
6879
  }
@@ -6433,7 +6881,10 @@ export class AgentSession {
6433
6881
  // Try promotion first — if a larger model is available, switch instead of compacting
6434
6882
  const promoted = await this.#tryContextPromotion(assistantMessage);
6435
6883
  if (!promoted) {
6436
- return await this.#runAutoCompaction("threshold", false, false, allowDefer, { autoContinue });
6884
+ return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
6885
+ autoContinue,
6886
+ triggerContextTokens: contextTokens,
6887
+ });
6437
6888
  }
6438
6889
  }
6439
6890
  return false;
@@ -6471,9 +6922,13 @@ export class AgentSession {
6471
6922
  this.#retryAttempt = 0;
6472
6923
  }
6473
6924
  this.#resolveRetry();
6925
+ // Tool-use orphans corrupt Anthropic message history (tool_result without
6926
+ // matching tool_use). Always remove them even when the retry cap is hit.
6927
+ if (assistantMessage.stopReason === "toolUse") {
6928
+ this.#removeEmptyStopFromActiveContext(assistantMessage);
6929
+ }
6474
6930
  return true;
6475
6931
  }
6476
-
6477
6932
  this.#removeEmptyStopFromActiveContext(assistantMessage);
6478
6933
  this.agent.appendMessage({
6479
6934
  role: "developer",
@@ -6486,12 +6941,27 @@ export class AgentSession {
6486
6941
  }
6487
6942
 
6488
6943
  #isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
6489
- if (assistantMessage.stopReason !== "stop") return false;
6490
- return !assistantMessage.content.some(content => {
6491
- if (content.type === "text") return content.text.trim().length > 0;
6492
- if (content.type === "thinking") return content.thinking.trim().length > 0;
6493
- return content.type === "toolCall";
6494
- });
6944
+ switch (assistantMessage.stopReason) {
6945
+ case "stop":
6946
+ // Reasoning/thinking-only turns are not actionable: they do not
6947
+ // answer the user and do not give the agent loop a tool call to run.
6948
+ for (const content of assistantMessage.content) {
6949
+ if (content.type === "toolCall") return false;
6950
+ if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6951
+ }
6952
+ return true;
6953
+ case "toolUse":
6954
+ // An orphaned toolUse stop (no tool_use block) corrupts Anthropic history:
6955
+ // a later tool_result has nothing to anchor to. Thinking alone cannot anchor
6956
+ // a tool_result, so it does not rescue a toolUse stop here.
6957
+ for (const content of assistantMessage.content) {
6958
+ if (content.type === "toolCall") return false;
6959
+ if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6960
+ }
6961
+ return true;
6962
+ default:
6963
+ return false;
6964
+ }
6495
6965
  }
6496
6966
 
6497
6967
  #emptyStopRetryReminder(): string {
@@ -6628,10 +7098,21 @@ export class AgentSession {
6628
7098
  });
6629
7099
  }
6630
7100
 
6631
- #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
6632
- const eagerTodosEnabled = this.settings.get("todo.eager");
7101
+ #buildEagerPreludeContext(): { toolRefs: Record<string, string>; taskBatch: boolean } {
7102
+ const wireName = (name: string): string => {
7103
+ const tool = this.#toolRegistry.get(name);
7104
+ return typeof tool?.customWireName === "string" ? tool.customWireName : name;
7105
+ };
7106
+ return {
7107
+ toolRefs: { task: wireName("task"), todo: wireName("todo") },
7108
+ taskBatch: this.settings.get("task.batch"),
7109
+ };
7110
+ }
7111
+
7112
+ #createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
7113
+ const mode = this.settings.get("todo.eager");
6633
7114
  const todosEnabled = this.settings.get("todo.enabled");
6634
- if (!eagerTodosEnabled || !todosEnabled) {
7115
+ if (mode === "default" || !todosEnabled) {
6635
7116
  return undefined;
6636
7117
  }
6637
7118
 
@@ -6655,36 +7136,63 @@ export class AgentSession {
6655
7136
  return undefined;
6656
7137
  }
6657
7138
 
6658
- if (!this.#toolRegistry.has("todo")) {
6659
- logger.warn("Eager todo enforcement skipped because todo is unavailable", {
6660
- activeToolNames: this.agent.state.tools.map(tool => tool.name),
7139
+ // Must check the active tool set, not just the registry: tool discovery
7140
+ // (tools.discoveryMode === "all") can register `todo` while hiding it from
7141
+ // the exposed tools. Forcing a named tool_choice for an inactive tool makes
7142
+ // the provider reject the request (HTTP 400).
7143
+ if (!this.getActiveToolNames().includes("todo")) {
7144
+ logger.warn("Eager todo enforcement skipped because todo is not active", {
7145
+ activeToolNames: this.getActiveToolNames(),
6661
7146
  });
6662
7147
  return undefined;
6663
7148
  }
6664
7149
 
7150
+ const message: AgentMessage = {
7151
+ role: "custom",
7152
+ customType: "eager-todo-prelude",
7153
+ content: prompt.render(eagerTodoPrompt, { ...this.#buildEagerPreludeContext(), forced: mode === "always" }),
7154
+ display: false,
7155
+ attribution: "agent",
7156
+ timestamp: Date.now(),
7157
+ };
7158
+ if (mode === "preferred") {
7159
+ return { message };
7160
+ }
6665
7161
  const todoToolChoice = buildNamedToolChoice("todo", this.model);
6666
7162
  if (!todoToolChoice) {
6667
- logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo", {
6668
- modelApi: this.model?.api,
6669
- modelId: this.model?.id,
6670
- });
6671
- return undefined;
7163
+ logger.warn(
7164
+ "Eager todo proceeding with the reminder only because the current model does not support a forced todo tool_choice",
7165
+ {
7166
+ modelApi: this.model?.api,
7167
+ modelId: this.model?.id,
7168
+ },
7169
+ );
7170
+ return { message };
6672
7171
  }
6673
-
6674
- const eagerTodoReminder = prompt.render(eagerTodoPrompt);
6675
-
6676
7172
  return {
6677
- message: {
6678
- role: "custom",
6679
- customType: "eager-todo-prelude",
6680
- content: eagerTodoReminder,
6681
- display: false,
6682
- attribution: "agent",
6683
- timestamp: Date.now(),
6684
- },
7173
+ message,
6685
7174
  toolChoice: todoToolChoice,
6686
7175
  };
6687
7176
  }
7177
+
7178
+ #createEagerTaskPrelude(promptText: string): AgentMessage | undefined {
7179
+ if (this.settings.get("task.eager") !== "always") return undefined;
7180
+ if (this.#agentKind === "sub") return undefined;
7181
+ if (this.#planModeState?.enabled) return undefined;
7182
+ if (this.agent.state.messages.some(m => m.role === "user")) return undefined;
7183
+ const trimmed = promptText.trimEnd();
7184
+ if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
7185
+ if (!this.getActiveToolNames().includes("task")) return undefined;
7186
+ return {
7187
+ role: "custom",
7188
+ customType: "eager-task-prelude",
7189
+ content: prompt.render(eagerTaskPrompt, this.#buildEagerPreludeContext()),
7190
+ display: false,
7191
+ attribution: "agent",
7192
+ timestamp: Date.now(),
7193
+ };
7194
+ }
7195
+
6688
7196
  /**
6689
7197
  * Check if agent stopped with incomplete todos and prompt to continue.
6690
7198
  */
@@ -6807,7 +7315,7 @@ export class AgentSession {
6807
7315
  const candidate = this.#resolveContextPromotionConfiguredTarget(currentModel, availableModels);
6808
7316
  if (!candidate) return undefined;
6809
7317
  if (modelsAreEqual(candidate, currentModel)) return undefined;
6810
- if (candidate.contextWindow <= contextWindow) return undefined;
7318
+ if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
6811
7319
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
6812
7320
  if (!apiKey) return undefined;
6813
7321
  return candidate;
@@ -6819,7 +7327,6 @@ export class AgentSession {
6819
7327
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
6820
7328
  }
6821
7329
  this.agent.setModel(model);
6822
- this.#syncToolCallBatchCap(model);
6823
7330
 
6824
7331
  // Re-evaluate append-only context mode — provider or setting may have changed
6825
7332
  this.#syncAppendOnlyContext(model);
@@ -6831,6 +7338,22 @@ export class AgentSession {
6831
7338
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
6832
7339
  }
6833
7340
 
7341
+ #resetCurrentResponsesProviderSession(reason: string): void {
7342
+ const currentModel = this.model;
7343
+ if (currentModel?.api !== "openai-responses" && currentModel?.api !== "openai-codex-responses") {
7344
+ return;
7345
+ }
7346
+
7347
+ this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
7348
+ this.agent.appendOnlyContext?.invalidateForModelChange();
7349
+ logger.debug("Reset Responses provider session after stale replay error", {
7350
+ provider: currentModel.provider,
7351
+ model: currentModel.id,
7352
+ api: currentModel.api,
7353
+ reason,
7354
+ });
7355
+ }
7356
+
6834
7357
  /**
6835
7358
  * Re-evaluate append-only context mode, creating or destroying the
6836
7359
  * manager as needed. Called on model switch AND setting change.
@@ -7075,7 +7598,7 @@ export class AgentSession {
7075
7598
 
7076
7599
  return resolveModelRoleValue(roleModelStr, availableModels, {
7077
7600
  settings: this.settings,
7078
- matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
7601
+ matchPreferences: getModelMatchPreferences(this.settings),
7079
7602
  modelRegistry: this.#modelRegistry,
7080
7603
  });
7081
7604
  }
@@ -7100,10 +7623,11 @@ export class AgentSession {
7100
7623
  // as auth fallbacks when the current model has no usable credentials.
7101
7624
  addCandidate(currentModel);
7102
7625
  for (const role of MODEL_ROLE_IDS) {
7626
+ if (MODEL_ROLES[role]?.hidden) continue;
7103
7627
  addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
7104
7628
  }
7105
7629
 
7106
- const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
7630
+ const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
7107
7631
  for (const model of sortedByContext) {
7108
7632
  if (!seen.has(this.#getModelKey(model))) {
7109
7633
  addCandidate(model);
@@ -7153,17 +7677,24 @@ export class AgentSession {
7153
7677
  if (!apiKey) continue;
7154
7678
 
7155
7679
  try {
7156
- return await compact(preparation, candidate, apiKey, customInstructions, signal, {
7157
- ...options,
7158
- metadata: this.agent.metadataForProvider(candidate.provider),
7159
- convertToLlm,
7160
- telemetry,
7161
- // Honor the user's /model thinking selection (incl. `off`) on
7162
- // the manual `/compact` path. Clamped per-model inside compact()
7163
- // via resolveCompactionEffort so unsupported-effort models
7164
- // (xai-oauth/grok-build) don't trip requireSupportedEffort.
7165
- thinkingLevel: this.thinkingLevel,
7166
- });
7680
+ return await compact(
7681
+ this.#obfuscatePreparationForProvider(preparation),
7682
+ candidate,
7683
+ this.#modelRegistry.resolver(candidate, this.sessionId),
7684
+ this.#obfuscateTextForProvider(customInstructions),
7685
+ signal,
7686
+ {
7687
+ ...options,
7688
+ metadata: this.agent.metadataForProvider(candidate.provider),
7689
+ convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
7690
+ telemetry,
7691
+ // Honor the user's /model thinking selection (incl. `off`) on
7692
+ // the manual `/compact` path. Clamped per-model inside compact()
7693
+ // via resolveCompactionEffort so unsupported-effort models
7694
+ // (xai-oauth/grok-build) don't trip requireSupportedEffort.
7695
+ thinkingLevel: this.thinkingLevel,
7696
+ },
7697
+ );
7167
7698
  } catch (error) {
7168
7699
  if (!this.#isCompactionAuthFailure(error)) {
7169
7700
  throw error;
@@ -7250,7 +7781,7 @@ export class AgentSession {
7250
7781
  willRetry: boolean,
7251
7782
  deferred = false,
7252
7783
  allowDefer = true,
7253
- options: { autoContinue?: boolean } = {},
7784
+ options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
7254
7785
  ): Promise<boolean> {
7255
7786
  const compactionSettings = this.settings.getGroup("compaction");
7256
7787
  if (compactionSettings.strategy === "off") return false;
@@ -7261,7 +7792,13 @@ export class AgentSession {
7261
7792
  // reclaims nothing we fall through to the summary-compaction body below so
7262
7793
  // the oversized input still gets resolved.
7263
7794
  if (compactionSettings.strategy === "shake") {
7264
- const outcome = await this.#runAutoShake(reason, willRetry, generation, shouldAutoContinue);
7795
+ const outcome = await this.#runAutoShake(
7796
+ reason,
7797
+ willRetry,
7798
+ generation,
7799
+ shouldAutoContinue,
7800
+ options.triggerContextTokens,
7801
+ );
7265
7802
  if (outcome !== "fallback") return false;
7266
7803
  }
7267
7804
  // "overflow" and "incomplete" force inline execution because they are recovery
@@ -7288,9 +7825,25 @@ export class AgentSession {
7288
7825
 
7289
7826
  // "overflow" forces context-full because the input itself is broken — a handoff
7290
7827
  // LLM call would hit the same overflow. "incomplete" is an output-side problem,
7291
- // so a handoff request on the existing context is still viable.
7292
- let action: "context-full" | "handoff" =
7828
+ // so a handoff request on the existing context is still viable. Snapcompact is
7829
+ // safe for every reason (it makes no LLM call at all) but requires a vision
7830
+ // model to be worth anything — fall back to context-full otherwise.
7831
+ let action: "context-full" | "handoff" | "snapcompact" =
7293
7832
  compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
7833
+ if (compactionSettings.strategy === "snapcompact") {
7834
+ if (this.model?.input.includes("image")) {
7835
+ action = "snapcompact";
7836
+ } else {
7837
+ logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
7838
+ model: this.model?.id,
7839
+ });
7840
+ this.emitNotice(
7841
+ "warning",
7842
+ `snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
7843
+ "compaction",
7844
+ );
7845
+ }
7846
+ }
7294
7847
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
7295
7848
  // Abort any older auto-compaction before installing this run's controller.
7296
7849
  this.#autoCompactionAbortController?.abort();
@@ -7429,6 +7982,21 @@ export class AgentSession {
7429
7982
  tokensBefore = compactionPrep.tokensBefore;
7430
7983
  details = compactionPrep.details;
7431
7984
  preserveData = compactionPrep.preserveData;
7985
+ } else if (action === "snapcompact") {
7986
+ // Local, deterministic: render discarded history onto PNG frames.
7987
+ // No model candidates, no API key, no retry loop.
7988
+ const snapcompactResult = await snapcompact.compact(preparation, {
7989
+ convertToLlm,
7990
+ model: this.model,
7991
+ thinkingLevel: this.thinkingLevel,
7992
+ maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
7993
+ });
7994
+ summary = snapcompactResult.summary;
7995
+ shortSummary = snapcompactResult.shortSummary;
7996
+ firstKeptEntryId = snapcompactResult.firstKeptEntryId;
7997
+ tokensBefore = snapcompactResult.tokensBefore;
7998
+ details = snapcompactResult.details;
7999
+ preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
7432
8000
  } else {
7433
8001
  const candidates = this.#getCompactionModelCandidates(availableModels);
7434
8002
  const retrySettings = this.settings.getGroup("retry");
@@ -7443,20 +8011,27 @@ export class AgentSession {
7443
8011
  let attempt = 0;
7444
8012
  while (true) {
7445
8013
  try {
7446
- compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
7447
- promptOverride: compactionPrep.hookPrompt,
7448
- extraContext: compactionPrep.hookContext,
7449
- remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
7450
- metadata: this.agent.metadataForProvider(candidate.provider),
7451
- initiatorOverride: "agent",
7452
- convertToLlm,
7453
- telemetry,
7454
- // Honor the user's /model thinking selection on the
7455
- // auto-compaction path — the most-fired compaction
7456
- // site. Clamped per-model inside compact() via
7457
- // resolveCompactionEffort.
7458
- thinkingLevel: this.thinkingLevel,
7459
- });
8014
+ compactResult = await compact(
8015
+ this.#obfuscatePreparationForProvider(preparation),
8016
+ candidate,
8017
+ this.#modelRegistry.resolver(candidate, this.sessionId),
8018
+ undefined,
8019
+ autoCompactionSignal,
8020
+ {
8021
+ promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
8022
+ extraContext: this.#obfuscateForProvider(compactionPrep.hookContext),
8023
+ remoteInstructions: this.#obfuscateForProvider(this.#baseSystemPrompt.join("\n\n")),
8024
+ metadata: this.agent.metadataForProvider(candidate.provider),
8025
+ initiatorOverride: "agent",
8026
+ convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
8027
+ telemetry,
8028
+ // Honor the user's /model thinking selection on the
8029
+ // auto-compaction path — the most-fired compaction
8030
+ // site. Clamped per-model inside compact() via
8031
+ // resolveCompactionEffort.
8032
+ thinkingLevel: this.thinkingLevel,
8033
+ },
8034
+ );
7460
8035
  break;
7461
8036
  } catch (error) {
7462
8037
  if (autoCompactionSignal.aborted) {
@@ -7661,6 +8236,7 @@ export class AgentSession {
7661
8236
  willRetry: boolean,
7662
8237
  generation: number,
7663
8238
  autoContinue: boolean,
8239
+ triggerContextTokens?: number,
7664
8240
  ): Promise<"handled" | "fallback"> {
7665
8241
  const action = "shake";
7666
8242
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
@@ -7681,16 +8257,52 @@ export class AgentSession {
7681
8257
  return "handled";
7682
8258
  }
7683
8259
  const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
7684
- // Overflow needs the input to actually shrink before the retry; if shake
7685
- // reclaimed nothing, summarization is the only remaining recovery.
7686
- if (reason === "overflow" && !reclaimed) {
8260
+ // Detect the dead-loop reported in issues #2119/#2275: the threshold check
8261
+ // fires, shake runs, but residual context is still above the configured
8262
+ // threshold. The next agent_end would re-trigger shake, which has nothing
8263
+ // new to drop on the second pass, so the loop spins until the user kills it.
8264
+ // Same hazard for "incomplete" (the retry would re-hit the length cap) and
8265
+ // for the existing "overflow + nothing reclaimed" case. In every recovery
8266
+ // reason we hand off to the summarization-driven context-full path so the
8267
+ // situation actually resolves; "idle" is exempt because its 60s+ timer
8268
+ // re-checks usage before re-firing and cannot dead-loop on its own.
8269
+ //
8270
+ // #2275: the post-shake check MUST be anchored on the same metric that
8271
+ // triggered compaction. The local estimator (`#estimatePendingPromptTokens`)
8272
+ // undercounts thinking-signature payloads, so on thinking-heavy sessions it
8273
+ // reads well below the provider-reported usage that fired the threshold.
8274
+ // When that estimate slips under the threshold, the fallback never fires
8275
+ // and the auto-continue prompt re-injects every turn. Prefer the trigger's
8276
+ // own `contextTokens` (provider-anchored) when the caller supplies it, and
8277
+ // add hysteresis (80% recovery band) so we don't oscillate at the boundary
8278
+ // while shake keeps reclaiming a trickle of the previous turn's output.
8279
+ const contextWindow = this.model?.contextWindow ?? 0;
8280
+ const compactionSettings = this.settings.getGroup("compaction");
8281
+ let stillOverThreshold = false;
8282
+ if (contextWindow > 0) {
8283
+ if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
8284
+ const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
8285
+ const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
8286
+ const recoveryBand = Math.floor(thresholdTokens * SHAKE_RECOVERY_BAND);
8287
+ stillOverThreshold = correctedTokens > recoveryBand;
8288
+ } else {
8289
+ const postShakeTokens = this.#estimatePendingPromptTokens([]);
8290
+ stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
8291
+ }
8292
+ }
8293
+ const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
8294
+ if (shouldFallBack) {
8295
+ const errorMessage = reclaimed
8296
+ ? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
8297
+ : "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
7687
8298
  await this.#emitSessionEvent({
7688
8299
  type: "auto_compaction_end",
7689
8300
  action,
7690
8301
  result: undefined,
7691
8302
  aborted: false,
7692
8303
  willRetry: false,
7693
- skipped: true,
8304
+ skipped: !reclaimed,
8305
+ errorMessage,
7694
8306
  });
7695
8307
  return "fallback";
7696
8308
  }
@@ -7788,10 +8400,40 @@ export class AgentSession {
7788
8400
  const contextWindow = this.model?.contextWindow ?? 0;
7789
8401
  if (isContextOverflow(message, contextWindow)) return false;
7790
8402
 
8403
+ if (this.#isClassifierRefusal(message)) return true;
8404
+ if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
8405
+
7791
8406
  const err = message.errorMessage;
7792
8407
  return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
7793
8408
  }
7794
8409
 
8410
+ #isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
8411
+ const currentApi = this.model?.api;
8412
+ if (
8413
+ message.api !== "openai-responses" &&
8414
+ message.api !== "openai-codex-responses" &&
8415
+ currentApi !== "openai-responses" &&
8416
+ currentApi !== "openai-codex-responses"
8417
+ ) {
8418
+ return false;
8419
+ }
8420
+
8421
+ const errorMessage = message.errorMessage;
8422
+ if (!errorMessage) return false;
8423
+
8424
+ return (
8425
+ /\bItem with id ['"][^'"]+['"] not found\.?/i.test(errorMessage) ||
8426
+ (/previous[ _]?response/i.test(errorMessage) &&
8427
+ /not[ _]?found|invalid|expired|stale|zero[ _-]?data[ _-]?retention/i.test(errorMessage))
8428
+ );
8429
+ }
8430
+
8431
+ #isClassifierRefusal(message: AssistantMessage): boolean {
8432
+ if (message.stopReason !== "error") return false;
8433
+ const stopType = message.stopDetails?.type;
8434
+ return stopType === "refusal" || stopType === "sensitive";
8435
+ }
8436
+
7795
8437
  #isTransientErrorMessage(errorMessage: string): boolean {
7796
8438
  return (
7797
8439
  this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
@@ -7806,11 +8448,12 @@ export class AgentSession {
7806
8448
  #isTransientTransportErrorMessage(errorMessage: string): boolean {
7807
8449
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
7808
8450
  // service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
7809
- // terminated, retry delay exceeded, Bun HTTP/2 stream resets (RST_STREAM / REFUSED_STREAM /
7810
- // ENHANCE_YOUR_CALM, surfaced verbatim from src/http/h2_client/dispatch.zig)
8451
+ // gateway upstream failures, terminated, retry delay exceeded, Bun HTTP/2 stream resets
8452
+ // (RST_STREAM / REFUSED_STREAM / ENHANCE_YOUR_CALM, surfaced verbatim from
8453
+ // src/http/h2_client/dispatch.zig)
7811
8454
  return (
7812
8455
  isUnexpectedSocketCloseMessage(errorMessage) ||
7813
- /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
8456
+ /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
7814
8457
  errorMessage,
7815
8458
  )
7816
8459
  );
@@ -7934,6 +8577,7 @@ export class AgentSession {
7934
8577
  role: string,
7935
8578
  selector: RetryFallbackSelector,
7936
8579
  currentSelector: string,
8580
+ options?: { pinFallback?: boolean },
7937
8581
  ): Promise<void> {
7938
8582
  const candidate = this.#modelRegistry.find(selector.provider, selector.id);
7939
8583
  if (!candidate) {
@@ -7959,9 +8603,11 @@ export class AgentSession {
7959
8603
  originalSelector: currentSelector,
7960
8604
  originalThinkingLevel: currentThinkingLevel,
7961
8605
  lastAppliedFallbackThinkingLevel: nextThinkingLevel,
8606
+ pinned: options?.pinFallback === true,
7962
8607
  };
7963
8608
  } else {
7964
8609
  this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
8610
+ this.#activeRetryFallback.pinned = this.#activeRetryFallback.pinned || options?.pinFallback === true;
7965
8611
  }
7966
8612
  await this.#emitSessionEvent({
7967
8613
  type: "retry_fallback_applied",
@@ -7971,7 +8617,7 @@ export class AgentSession {
7971
8617
  });
7972
8618
  }
7973
8619
 
7974
- async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
8620
+ async #tryRetryModelFallback(currentSelector: string, options?: { pinFallback?: boolean }): Promise<boolean> {
7975
8621
  const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
7976
8622
  if (!role) return false;
7977
8623
 
@@ -7981,7 +8627,7 @@ export class AgentSession {
7981
8627
  if (!candidate) continue;
7982
8628
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
7983
8629
  if (!apiKey) continue;
7984
- await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
8630
+ await this.#applyRetryFallbackCandidate(role, selector, currentSelector, options);
7985
8631
  return true;
7986
8632
  }
7987
8633
 
@@ -7990,6 +8636,7 @@ export class AgentSession {
7990
8636
 
7991
8637
  async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
7992
8638
  if (!this.#activeRetryFallback) return;
8639
+ if (this.#activeRetryFallback.pinned) return;
7993
8640
  if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
7994
8641
 
7995
8642
  const {
@@ -8087,6 +8734,7 @@ export class AgentSession {
8087
8734
  async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
8088
8735
  const retrySettings = this.settings.getGroup("retry");
8089
8736
  if (!retrySettings.enabled) return false;
8737
+ const classifierRefusal = this.#isClassifierRefusal(message);
8090
8738
 
8091
8739
  const generation = this.#promptGeneration;
8092
8740
  this.#retryAttempt++;
@@ -8113,42 +8761,83 @@ export class AgentSession {
8113
8761
  }
8114
8762
 
8115
8763
  const errorMessage = message.errorMessage || "Unknown error";
8764
+ const staleOpenAIResponsesReplayError = this.#isStaleOpenAIResponsesReplayError(message);
8116
8765
  const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
8117
- let delayMs = retrySettings.baseDelayMs * 2 ** (this.#retryAttempt - 1);
8766
+ let delayMs = staleOpenAIResponsesReplayError
8767
+ ? 0
8768
+ : calculateRetryBackoffDelayMs(retrySettings.baseDelayMs, this.#retryAttempt);
8118
8769
  let switchedCredential = false;
8119
8770
  let switchedModel = false;
8771
+ // Set when a usage-limit error pinned the wait to credential
8772
+ // availability — suppresses the generic retry-after bump below.
8773
+ let usageLimitWaitMs: number | undefined;
8774
+
8775
+ if (staleOpenAIResponsesReplayError) {
8776
+ this.#resetCurrentResponsesProviderSession("stale replay error");
8777
+ }
8120
8778
 
8121
- if (this.model && isUsageLimitError(errorMessage)) {
8779
+ if (this.model && !staleOpenAIResponsesReplayError && isUsageLimitError(errorMessage)) {
8122
8780
  const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
8123
- const switched = await this.#modelRegistry.authStorage.markUsageLimitReached(
8781
+ const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
8124
8782
  this.model.provider,
8125
8783
  this.sessionId,
8126
8784
  {
8127
8785
  retryAfterMs,
8128
8786
  baseUrl: this.model.baseUrl,
8787
+ modelId: this.model.id,
8129
8788
  },
8130
8789
  );
8131
- if (switched) {
8790
+ if (outcome.switched) {
8132
8791
  switchedCredential = true;
8133
8792
  delayMs = 0;
8134
- } else if (retryAfterMs > delayMs) {
8135
- // No more accounts to switch to wait out the backoff
8136
- delayMs = retryAfterMs;
8793
+ } else if (await this.#maybeAutoRedeemCodexReset()) {
8794
+ // A live usage-limit 429 on the active Codex account, with a banked
8795
+ // reset and the opt-in setting on: spend the reset and retry
8796
+ // immediately instead of waiting out the window. Runs after the
8797
+ // free sibling-switch above and before model fallback below.
8798
+ switchedCredential = true;
8799
+ delayMs = 0;
8800
+ } else {
8801
+ // No sibling credential is usable right now. Wait for whichever
8802
+ // comes first: the provider's retry-after window for the current
8803
+ // account, or the earliest moment a temporarily blocked sibling
8804
+ // frees up (e.g. a 60s post-401 block or a 5-min usage-probe
8805
+ // block) — the next attempt's getApiKey re-ranks and picks it up.
8806
+ // Without this, one short-lived sibling block escalates a
8807
+ // recoverable situation into the provider's multi-hour wait and
8808
+ // trips the fail-fast cap below.
8809
+ usageLimitWaitMs = retryAfterMs;
8810
+ if (outcome.retryAtMs !== undefined) {
8811
+ const siblingWaitMs = Math.max(0, outcome.retryAtMs - Date.now()) + SIBLING_UNBLOCK_BUFFER_MS;
8812
+ if (siblingWaitMs < usageLimitWaitMs) {
8813
+ usageLimitWaitMs = siblingWaitMs;
8814
+ }
8815
+ }
8816
+ if (usageLimitWaitMs > delayMs) {
8817
+ delayMs = usageLimitWaitMs;
8818
+ }
8137
8819
  }
8138
8820
  }
8139
8821
 
8140
8822
  const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
8141
- if (!switchedCredential && currentSelector) {
8823
+ if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
8142
8824
  if (retrySettings.modelFallback) {
8143
- this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
8144
- switchedModel = await this.#tryRetryModelFallback(currentSelector);
8825
+ if (!classifierRefusal) {
8826
+ this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
8827
+ }
8828
+ switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
8145
8829
  }
8146
8830
  if (switchedModel) {
8147
8831
  delayMs = 0;
8148
- } else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
8832
+ } else if (usageLimitWaitMs === undefined && parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
8149
8833
  delayMs = parsedRetryAfterMs;
8150
8834
  }
8151
8835
  }
8836
+ if (classifierRefusal && !switchedModel) {
8837
+ this.#retryAttempt = 0;
8838
+ this.#resolveRetry();
8839
+ return false;
8840
+ }
8152
8841
 
8153
8842
  // Fail-fast cap: if the provider asks us to wait longer than
8154
8843
  // retry.maxDelayMs and we have no fallback credential or model to
@@ -8306,11 +8995,12 @@ export class AgentSession {
8306
8995
  * @param command The bash command to execute
8307
8996
  * @param onChunk Optional streaming callback for output
8308
8997
  * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
8998
+ * @param options.useUserShell If true, allow caller to request configured user-shell routing
8309
8999
  */
8310
9000
  async executeBash(
8311
9001
  command: string,
8312
9002
  onChunk?: (chunk: string) => void,
8313
- options?: { excludeFromContext?: boolean },
9003
+ options?: { excludeFromContext?: boolean; useUserShell?: boolean },
8314
9004
  ): Promise<BashResult> {
8315
9005
  const excludeFromContext = options?.excludeFromContext === true;
8316
9006
  const cwd = this.sessionManager.getCwd();
@@ -8338,6 +9028,7 @@ export class AgentSession {
8338
9028
  sessionKey: this.sessionId,
8339
9029
  timeout: clampTimeout("bash") * 1000,
8340
9030
  onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
9031
+ useUserShell: options?.useUserShell,
8341
9032
  });
8342
9033
 
8343
9034
  this.recordBashResult(command, result, options);
@@ -8463,6 +9154,7 @@ export class AgentSession {
8463
9154
  sessionId: namespacePythonSessionId(sessionId),
8464
9155
  kernelOwnerId: this.#evalKernelOwnerId,
8465
9156
  kernelMode: this.settings.get("python.kernelMode"),
9157
+ interpreter: this.settings.get("python.interpreter")?.trim() || undefined,
8466
9158
  onChunk,
8467
9159
  signal: abortController.signal,
8468
9160
  });
@@ -8589,118 +9281,111 @@ export class AgentSession {
8589
9281
  }
8590
9282
 
8591
9283
  // =========================================================================
8592
- // Background-Channel IRC Exchanges
9284
+ // IRC Delivery
8593
9285
  // =========================================================================
8594
9286
 
8595
9287
  /**
8596
- * Generate an ephemeral reply to a background message (e.g. an IRC ping from
8597
- * another agent) using this session's current model + system prompt + history.
9288
+ * Deliver an IRC message into this session (recipient side; called by the
9289
+ * IrcBus). Emits the `irc_message` session event for UI cards and injects
9290
+ * the rendered message into the model's context as an `irc:incoming`
9291
+ * custom message:
9292
+ *
9293
+ * - mid-turn → queued on the aside channel and folded in at the next step
9294
+ * boundary (non-interrupting, like async-result deliveries) → "injected";
9295
+ * - idle → starts a real turn with the message so the recipient wakes
9296
+ * → "woken".
9297
+ *
9298
+ * Never blocks on the recipient's turn: the wake turn is fire-and-forget.
8598
9299
  *
8599
- * The incoming message is queued for injection into the recipient's persisted
8600
- * history immediately so timeouts/abort still preserve delivery. The reply is
8601
- * computed via a side-channel `streamSimple` call (analogous to `/btw`) so it
8602
- * never blocks on the recipient's in-flight tool calls. When a reply is
8603
- * generated, it is queued separately. Injection happens immediately when the
8604
- * session is idle, otherwise it is deferred until streaming ends.
9300
+ * When the sender expects a reply (`send await:true`) and this session is
9301
+ * mid-turn with async execution disabled, the next step boundary may be
9302
+ * gated on the sender's own batch finishing (blocking task spawns), so a
9303
+ * real reply turn can never happen in time. In that case an ephemeral
9304
+ * side-channel auto-reply is generated from the current context (the old
9305
+ * `respondAsBackground` path) and sent back over the bus on this agent's
9306
+ * behalf.
8605
9307
  */
8606
- async respondAsBackground(args: {
8607
- from: string;
8608
- message: string;
8609
- awaitReply?: boolean;
8610
- signal?: AbortSignal;
8611
- }): Promise<{ replyText: string | null }> {
8612
- const awaitReply = args.awaitReply !== false;
8613
- const incomingTimestamp = Date.now();
8614
- const incomingRecord: CustomMessage = {
8615
- role: "custom",
8616
- customType: "irc:incoming",
8617
- content: `[IRC \`${args.from}\` → you]\n\n${args.message}`,
8618
- display: true,
8619
- details: { from: args.from, message: args.message },
8620
- attribution: "agent",
8621
- timestamp: incomingTimestamp,
8622
- };
8623
- void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
8624
- this.#forwardIrcRelayToMain({
8625
- from: args.from,
8626
- to: this.#agentId ?? "?",
8627
- body: args.message,
8628
- kind: "message",
8629
- timestamp: incomingTimestamp,
8630
- });
8631
-
8632
- this.#queueBackgroundExchangeInjection([incomingRecord]);
8633
- if (!awaitReply) {
8634
- return { replyText: null };
9308
+ async deliverIrcMessage(msg: IrcMessage, opts?: { expectsReply?: boolean }): Promise<"injected" | "woken"> {
9309
+ if (this.#isDisposed) {
9310
+ throw new Error("Recipient session is disposed.");
8635
9311
  }
8636
-
8637
- const incomingPrompt = prompt.render(ircIncomingTemplate, {
8638
- from: args.from,
8639
- message: args.message,
8640
- });
8641
- const { replyText } = await this.runEphemeralTurn({
8642
- promptText: incomingPrompt,
8643
- signal: args.signal,
8644
- });
8645
-
8646
- const replyRecord: CustomMessage = {
9312
+ const autoReply = (opts?.expectsReply ?? false) && this.isStreaming && !this.settings.get("async.enabled");
9313
+ const record: CustomMessage = {
8647
9314
  role: "custom",
8648
- customType: "irc:autoreply",
8649
- content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
9315
+ customType: "irc:incoming",
9316
+ content: prompt.render(ircIncomingTemplate, {
9317
+ from: msg.from,
9318
+ message: msg.body,
9319
+ replyTo: msg.replyTo ?? "",
9320
+ autoReplied: autoReply,
9321
+ }),
8650
9322
  display: true,
8651
- details: { to: args.from, reply: replyText },
9323
+ details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
8652
9324
  attribution: "agent",
8653
- timestamp: Date.now(),
9325
+ timestamp: msg.ts,
8654
9326
  };
8655
- void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
8656
- this.#forwardIrcRelayToMain({
8657
- from: this.#agentId ?? "?",
8658
- to: args.from,
8659
- body: replyText,
8660
- kind: "reply",
8661
- timestamp: replyRecord.timestamp,
9327
+ void this.#emitSessionEvent({ type: "irc_message", message: record });
9328
+ if (this.isStreaming) {
9329
+ this.#pendingIrcAsides.push(record);
9330
+ if (autoReply) void this.#runIrcAutoReply(msg);
9331
+ return "injected";
9332
+ }
9333
+ // Idle: same wake primitive the yield queue uses for async-result
9334
+ // delivery — prompt the agent directly so a real turn runs.
9335
+ this.agent.prompt(record).catch(error => {
9336
+ logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
8662
9337
  });
8663
- this.#queueBackgroundExchangeInjection([replyRecord]);
8664
-
8665
- return { replyText };
9338
+ return "woken";
8666
9339
  }
8667
9340
 
8668
9341
  /**
8669
- * Forward an IRC exchange observation to the main agent's session UI so the
8670
- * user can see every IRC conversation in the main transcript, even when the
8671
- * main agent is not a direct participant. The relay record is display-only:
8672
- * it is NOT injected into the main agent's persisted history.
9342
+ * Generate and deliver an ephemeral auto-reply to `msg` on this agent's
9343
+ * behalf: a no-tools side-channel turn over the current history (same
9344
+ * pipeline as `/btw`), recorded into this session as an `irc:autoreply`
9345
+ * aside so the model knows what was said for it, and sent back to the
9346
+ * sender as a regular bus message (`replyTo: msg.id`) so their parked
9347
+ * `wait`/`await:true` resolves. Failures only log — the sender then hits
9348
+ * its normal wait timeout.
8673
9349
  */
8674
- #forwardIrcRelayToMain(args: {
8675
- from: string;
8676
- to: string;
8677
- body: string;
8678
- kind: "message" | "reply";
8679
- timestamp: number;
8680
- }): void {
8681
- const registry = this.#agentRegistry;
8682
- if (!registry) return;
8683
- // If this session is the main agent, the local emit already reached the main UI.
8684
- if (this.#agentId === MAIN_AGENT_ID) return;
8685
- const mainRef = registry.get(MAIN_AGENT_ID);
8686
- const mainSession = mainRef?.session;
8687
- if (!mainSession || mainSession === this) return;
8688
- const arrow = args.kind === "reply" ? "→ (auto)" : "→";
8689
- const relayRecord: CustomMessage = {
8690
- role: "custom",
8691
- customType: "irc:relay",
8692
- content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
8693
- display: true,
8694
- details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
8695
- attribution: "agent",
8696
- timestamp: args.timestamp,
8697
- };
8698
- mainSession.emitIrcRelayObservation(relayRecord);
9350
+ async #runIrcAutoReply(msg: IrcMessage): Promise<void> {
9351
+ try {
9352
+ const { replyText } = await this.runEphemeralTurn({
9353
+ promptText: prompt.render(ircAutoReplyTemplate, {
9354
+ from: msg.from,
9355
+ message: msg.body,
9356
+ replyTo: msg.replyTo ?? "",
9357
+ }),
9358
+ });
9359
+ const body = replyText.trim();
9360
+ if (!body || this.#isDisposed) return;
9361
+ const record: CustomMessage = {
9362
+ role: "custom",
9363
+ customType: "irc:autoreply",
9364
+ content: `[IRC you → \`${msg.from}\` (auto)]\n\n${body}`,
9365
+ display: true,
9366
+ details: { to: msg.from, body, replyTo: msg.id },
9367
+ attribution: "agent",
9368
+ timestamp: Date.now(),
9369
+ };
9370
+ void this.#emitSessionEvent({ type: "irc_message", message: record });
9371
+ // Asides drain at the next step boundary; anything left over is
9372
+ // flushed at the start of the next prompt (#flushPendingIrcAsides).
9373
+ this.#pendingIrcAsides.push(record);
9374
+ // `from` must be the id the sender addressed (msg.to) so their
9375
+ // from-filtered waiter matches.
9376
+ const receipt = await IrcBus.global().send({ from: msg.to, to: msg.from, body, replyTo: msg.id });
9377
+ if (receipt.outcome === "failed") {
9378
+ logger.warn("IRC auto-reply delivery failed", { to: msg.from, error: receipt.error });
9379
+ }
9380
+ } catch (error) {
9381
+ logger.warn("IRC auto-reply turn failed", { from: msg.from, error: String(error) });
9382
+ }
8699
9383
  }
8700
9384
 
8701
9385
  /**
8702
9386
  * Emit an IRC relay observation event on this session for UI rendering only.
8703
- * Does not persist the record to history. Public so other sessions can forward.
9387
+ * Does not persist the record to history. Called by the IrcBus to surface
9388
+ * agent↔agent traffic on the main session.
8704
9389
  */
8705
9390
  emitIrcRelayObservation(record: CustomMessage): void {
8706
9391
  void this.#emitSessionEvent({ type: "irc_message", message: record });
@@ -8712,14 +9397,13 @@ export class AgentSession {
8712
9397
  * does not block on, or interfere with, any in-flight main turn. The
8713
9398
  * session's history and persisted state are NOT modified by this call.
8714
9399
  *
8715
- * Used by `respondAsBackground` (IRC) and `BtwController` (`/btw`) to share
9400
+ * Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
8716
9401
  * the snapshot + stream pipeline. The snapshot includes any in-flight
8717
9402
  * streaming assistant text so the model sees the half-finished response
8718
9403
  * rather than missing context.
8719
9404
  */
8720
9405
  async runEphemeralTurn(args: {
8721
9406
  promptText: string;
8722
- images?: ImageContent[];
8723
9407
  onTextDelta?: (delta: string) => void;
8724
9408
  signal?: AbortSignal;
8725
9409
  dedupeReply?: boolean;
@@ -8733,7 +9417,7 @@ export class AgentSession {
8733
9417
  throw new Error(`No API key for ${model.provider}/${model.id}`);
8734
9418
  }
8735
9419
 
8736
- const snapshot = this.#buildEphemeralSnapshot(args.promptText, args.images);
9420
+ const snapshot = this.#buildEphemeralSnapshot(args.promptText);
8737
9421
  const llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
8738
9422
  const context: Context = {
8739
9423
  systemPrompt: this.systemPrompt,
@@ -8756,6 +9440,7 @@ export class AgentSession {
8756
9440
  promptCacheKey: cacheSessionId,
8757
9441
  preferWebsockets: false,
8758
9442
  reasoning: toReasoningEffort(this.thinkingLevel),
9443
+ disableReasoning: shouldDisableReasoning(this.thinkingLevel),
8759
9444
  hideThinkingSummary: this.agent.hideThinkingSummary,
8760
9445
  serviceTier: this.serviceTier,
8761
9446
  signal: args.signal,
@@ -8764,17 +9449,27 @@ export class AgentSession {
8764
9449
  model.provider,
8765
9450
  );
8766
9451
 
8767
- let replyText = "";
9452
+ let providerReplyText = "";
9453
+ let emittedReplyText = "";
8768
9454
  let assistantMessage: AssistantMessage | undefined;
8769
- const stream = streamSimple(model, context, options);
9455
+ const stream = streamSimple(model, obfuscateProviderContext(this.#obfuscator, context), options);
8770
9456
  for await (const event of stream) {
8771
9457
  if (event.type === "text_delta") {
8772
- replyText += event.delta;
8773
- if (args.onTextDelta) args.onTextDelta(event.delta);
9458
+ providerReplyText += event.delta;
9459
+ if (args.onTextDelta) {
9460
+ const readyText = this.#deobfuscatedProviderTextReadyForDelta(providerReplyText);
9461
+ if (readyText.length > emittedReplyText.length) {
9462
+ const delta = readyText.slice(emittedReplyText.length);
9463
+ emittedReplyText = readyText;
9464
+ args.onTextDelta(delta);
9465
+ }
9466
+ }
8774
9467
  continue;
8775
9468
  }
8776
9469
  if (event.type === "done") {
8777
- assistantMessage = event.message;
9470
+ assistantMessage = this.#obfuscator?.hasSecrets()
9471
+ ? { ...event.message, content: this.#obfuscator.deobfuscateObject(event.message.content) }
9472
+ : event.message;
8778
9473
  break;
8779
9474
  }
8780
9475
  if (event.type === "error") {
@@ -8785,8 +9480,12 @@ export class AgentSession {
8785
9480
  if (!assistantMessage) {
8786
9481
  throw new Error("Ephemeral turn ended without a final message");
8787
9482
  }
9483
+ const replyText = this.#deobfuscateFromProvider(providerReplyText);
9484
+ if (args.onTextDelta && replyText.length > emittedReplyText.length) {
9485
+ args.onTextDelta(replyText.slice(emittedReplyText.length));
9486
+ }
8788
9487
  return {
8789
- replyText: args.dedupeReply === false ? replyText.trim() : dedupeIrcReply(replyText.trim()),
9488
+ replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
8790
9489
  assistantMessage,
8791
9490
  };
8792
9491
  }
@@ -8797,7 +9496,7 @@ export class AgentSession {
8797
9496
  * the partial response in context, then appends the prompt as a virtual
8798
9497
  * user message.
8799
9498
  */
8800
- #buildEphemeralSnapshot(promptText: string, images?: ImageContent[]): AgentMessage[] {
9499
+ #buildEphemeralSnapshot(promptText: string): AgentMessage[] {
8801
9500
  const messages = [...this.messages];
8802
9501
  const streaming = this.agent.state.streamMessage;
8803
9502
  if (streaming && streaming.role === "assistant") {
@@ -8828,59 +9527,30 @@ export class AgentSession {
8828
9527
  }
8829
9528
  }
8830
9529
  }
8831
- const content: (TextContent | ImageContent)[] = [{ type: "text", text: promptText }];
8832
- if (images && images.length > 0) {
8833
- content.push(...images);
8834
- }
8835
9530
  messages.push({
8836
9531
  role: "user",
8837
- content,
9532
+ content: [{ type: "text", text: promptText }],
8838
9533
  attribution: "agent",
8839
9534
  timestamp: Date.now(),
8840
9535
  });
8841
9536
  return messages;
8842
9537
  }
8843
9538
 
8844
- #queueBackgroundExchangeInjection(messages: CustomMessage[]): void {
8845
- this.#pendingBackgroundExchanges.push(messages);
8846
- if (!this.isStreaming) {
8847
- this.#flushPendingBackgroundExchanges();
8848
- return;
8849
- }
8850
- this.#scheduleBackgroundExchangeFlush();
8851
- }
8852
-
8853
- #scheduleBackgroundExchangeFlush(): void {
8854
- if (this.#scheduledBackgroundExchangeFlush) return;
8855
- this.#scheduledBackgroundExchangeFlush = true;
8856
- const attempt = (): void => {
8857
- if (this.#pendingBackgroundExchanges.length === 0 || this.#isDisposed) {
8858
- this.#pendingBackgroundExchanges = [];
8859
- this.#scheduledBackgroundExchangeFlush = false;
8860
- return;
8861
- }
8862
- if (this.isStreaming) {
8863
- setTimeout(attempt, 50);
8864
- return;
8865
- }
8866
- this.#scheduledBackgroundExchangeFlush = false;
8867
- this.#flushPendingBackgroundExchanges();
8868
- };
8869
- setTimeout(attempt, 0);
8870
- }
8871
-
8872
- #flushPendingBackgroundExchanges(): void {
8873
- if (this.#pendingBackgroundExchanges.length === 0) return;
8874
- const batches = this.#pendingBackgroundExchanges;
8875
- this.#pendingBackgroundExchanges = [];
8876
- for (const batch of batches) {
8877
- for (const msg of batch) {
8878
- // emitExternalEvent on message_end appends to agent state and dispatches
8879
- // to all session listeners, which in turn handle TUI rendering and
8880
- // sessionManager persistence via #handleAgentEvent.
8881
- this.agent.emitExternalEvent({ type: "message_start", message: msg });
8882
- this.agent.emitExternalEvent({ type: "message_end", message: msg });
8883
- }
9539
+ /**
9540
+ * Persist any IRC asides that missed their step-boundary injection (the
9541
+ * message landed after the turn's last aside drain). Called at the start
9542
+ * of the next prompt so the model still sees them.
9543
+ */
9544
+ #flushPendingIrcAsides(): void {
9545
+ if (this.#pendingIrcAsides.length === 0) return;
9546
+ const records = this.#pendingIrcAsides;
9547
+ this.#pendingIrcAsides = [];
9548
+ for (const record of records) {
9549
+ // emitExternalEvent on message_end appends to agent state and dispatches
9550
+ // to all session listeners, which in turn handle TUI rendering and
9551
+ // sessionManager persistence via #handleAgentEvent.
9552
+ this.agent.emitExternalEvent({ type: "message_start", message: record });
9553
+ this.agent.emitExternalEvent({ type: "message_end", message: record });
8884
9554
  }
8885
9555
  }
8886
9556
 
@@ -8935,8 +9605,8 @@ export class AgentSession {
8935
9605
  // the existing message objects is sufficient and avoids structured-clone failures for
8936
9606
  // extension/custom metadata that is valid to persist but not cloneable.
8937
9607
  const previousAgentMessages = [...this.agent.state.messages];
8938
- const previousSteeringMessages = [...this.#steeringMessages];
8939
- const previousFollowUpMessages = [...this.#followUpMessages];
9608
+ const previousSteeringMessages = [...this.agent.peekSteeringQueue()];
9609
+ const previousFollowUpMessages = [...this.agent.peekFollowUpQueue()];
8940
9610
  const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
8941
9611
  const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
8942
9612
  const previousModel = this.model;
@@ -8948,17 +9618,20 @@ export class AgentSession {
8948
9618
  const previousTools = [...this.agent.state.tools];
8949
9619
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
8950
9620
  const previousSystemPrompt = this.agent.state.systemPrompt;
9621
+ const previousFreshProviderSessionId = this.#freshProviderSessionId;
8951
9622
  const previousFallbackSelectedMCPToolNames = previousSessionFile
8952
9623
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
8953
9624
  : undefined;
8954
9625
 
8955
- this.#steeringMessages = [];
8956
- this.#followUpMessages = [];
9626
+ this.agent.clearAllQueues();
8957
9627
  this.#pendingNextTurnMessages = [];
8958
9628
  this.#scheduledHiddenNextTurnGeneration = undefined;
8959
9629
 
8960
9630
  try {
8961
9631
  await this.sessionManager.setSessionFile(sessionPath);
9632
+ if (switchingToDifferentSession) {
9633
+ this.#freshProviderSessionId = undefined;
9634
+ }
8962
9635
  this.#syncAgentSessionId();
8963
9636
  this.#rekeyHindsightMemoryForCurrentSessionId();
8964
9637
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9015,7 +9688,6 @@ export class AgentSession {
9015
9688
  this.#setModelWithProviderSessionReset(match);
9016
9689
  } else {
9017
9690
  this.agent.setModel(match);
9018
- this.#syncToolCallBatchCap(match);
9019
9691
  }
9020
9692
  }
9021
9693
  }
@@ -9045,7 +9717,7 @@ export class AgentSession {
9045
9717
  this.#autoResolvedLevel = undefined;
9046
9718
  this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
9047
9719
  }
9048
- this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel));
9720
+ this.#applyThinkingLevelToAgent(this.#thinkingLevel);
9049
9721
  this.agent.serviceTier = hasServiceTierEntry
9050
9722
  ? sessionContext.serviceTier
9051
9723
  : configuredServiceTier === "none"
@@ -9068,6 +9740,7 @@ export class AgentSession {
9068
9740
  return true;
9069
9741
  } catch (error) {
9070
9742
  this.sessionManager.restoreState(previousSessionState);
9743
+ this.#freshProviderSessionId = previousFreshProviderSessionId;
9071
9744
  this.#syncAgentSessionId(previousSessionState.sessionId);
9072
9745
  this.#rekeyHindsightMemoryForCurrentSessionId();
9073
9746
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9091,20 +9764,16 @@ export class AgentSession {
9091
9764
  this.#baseSystemPrompt = previousBaseSystemPrompt;
9092
9765
  this.agent.setSystemPrompt(previousSystemPrompt);
9093
9766
  this.agent.replaceMessages(previousAgentMessages);
9094
- this.#steeringMessages = previousSteeringMessages;
9095
- this.#followUpMessages = previousFollowUpMessages;
9767
+ this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
9096
9768
  this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
9097
9769
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
9098
9770
  if (previousModel) {
9099
9771
  this.agent.setModel(previousModel);
9100
- this.#syncToolCallBatchCap(previousModel);
9101
- } else {
9102
- this.#syncToolCallBatchCap(undefined);
9103
9772
  }
9104
9773
  this.#thinkingLevel = previousThinkingLevel;
9105
9774
  this.#autoThinking = previousAutoThinking;
9106
9775
  this.#autoResolvedLevel = previousAutoResolvedLevel;
9107
- this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
9776
+ this.#applyThinkingLevelToAgent(previousThinkingLevel);
9108
9777
  this.agent.serviceTier = previousServiceTier;
9109
9778
  this.#syncTodoPhasesFromBranch();
9110
9779
  this.#reconnectToAgent();
@@ -9166,6 +9835,7 @@ export class AgentSession {
9166
9835
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
9167
9836
  }
9168
9837
  this.#syncTodoPhasesFromBranch();
9838
+ this.#freshProviderSessionId = undefined;
9169
9839
  this.#syncAgentSessionId();
9170
9840
  this.#rekeyHindsightMemoryForCurrentSessionId();
9171
9841
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9285,12 +9955,12 @@ export class AgentSession {
9285
9955
  const branchSummarySettings = this.settings.getGroup("branchSummary");
9286
9956
  const result = await generateBranchSummary(entriesToSummarize, {
9287
9957
  model,
9288
- apiKey,
9958
+ apiKey: this.#modelRegistry.resolver(model, this.sessionId),
9289
9959
  signal: this.#branchSummaryAbortController.signal,
9290
- customInstructions: options.customInstructions,
9960
+ customInstructions: this.#obfuscateTextForProvider(options.customInstructions),
9291
9961
  reserveTokens: branchSummarySettings.reserveTokens,
9292
9962
  metadata: this.agent.metadataForProvider(model.provider),
9293
- convertToLlm,
9963
+ convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
9294
9964
  telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
9295
9965
  });
9296
9966
  this.#branchSummaryAbortController = undefined;
@@ -9542,6 +10212,169 @@ export class AgentSession {
9542
10212
  });
9543
10213
  }
9544
10214
 
10215
+ /**
10216
+ * Redeem one saved Codex rate-limit reset for a specific account, injecting
10217
+ * the provider base URL like {@link AgentSession.fetchUsageReports}. Powers
10218
+ * the `/usage reset` command and auto-redeem. Never throws for business
10219
+ * outcomes — inspect the returned `code`.
10220
+ */
10221
+ async redeemResetCredit(target: ResetCreditTarget, signal?: AbortSignal): Promise<ResetCreditRedeemOutcome> {
10222
+ return this.#modelRegistry.authStorage.redeemResetCredit({
10223
+ target,
10224
+ baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
10225
+ signal,
10226
+ });
10227
+ }
10228
+
10229
+ /**
10230
+ * List saved Codex rate-limit resets per stored account, fetched live from
10231
+ * the dedicated credits endpoint (bypasses the usage cache). Powers the
10232
+ * `/usage reset` account selector.
10233
+ */
10234
+ async listResetCredits(signal?: AbortSignal): Promise<ResetCreditAccountStatus[]> {
10235
+ return this.#modelRegistry.authStorage.listResetCredits({
10236
+ sessionId: this.sessionId,
10237
+ baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
10238
+ signal,
10239
+ });
10240
+ }
10241
+ async #confirmCodexAutoRedeem(decision: CodexAutoRedeemRedeemDecision): Promise<boolean> {
10242
+ const runner = this.#extensionRunner;
10243
+ if (!runner?.hasUI()) {
10244
+ this.emitNotice(
10245
+ "warning",
10246
+ "Codex saved reset is eligible, but auto-redeem is unset and no prompt UI is available. Run `/usage reset` or set codexResets.autoRedeem.",
10247
+ "codex-auto-reset",
10248
+ );
10249
+ return false;
10250
+ }
10251
+
10252
+ const who = decision.target.email ?? decision.target.accountId ?? "the active account";
10253
+ const resetLabel = decision.availableCount === 1 ? "reset" : "resets";
10254
+ try {
10255
+ const choice = await runner
10256
+ .getUIContext()
10257
+ .select(
10258
+ `Do you wanna redeem your reset?\n${who} is blocked by the weekly Codex limit for about ${formatDuration(decision.remainingMs)}. Spend 1 of ${decision.availableCount} saved ${resetLabel}?`,
10259
+ [
10260
+ {
10261
+ label: "Yes",
10262
+ description: "Redeem now and remember yes for future eligible Codex weekly blocks.",
10263
+ },
10264
+ {
10265
+ label: "No",
10266
+ description: "Do not auto-redeem saved Codex resets.",
10267
+ },
10268
+ ],
10269
+ );
10270
+ if (choice === "Yes") {
10271
+ this.settings.set("codexResets.autoRedeem", "yes");
10272
+ return true;
10273
+ }
10274
+ if (choice === "No") {
10275
+ this.settings.set("codexResets.autoRedeem", "no");
10276
+ }
10277
+ } catch (error) {
10278
+ logger.warn("codex-auto-reset prompt failed", { error: String(error) });
10279
+ }
10280
+ return false;
10281
+ }
10282
+
10283
+ /**
10284
+ * Auto-redeem hook for {@link AgentSession.#handleRetryableError}'s
10285
+ * usage-limit branch. Returns `true` only when a saved Codex reset was
10286
+ * actually spent (so the caller retries immediately). The "unset" mode is
10287
+ * reactive but asks before spending; "yes" skips that prompt, and "no" avoids
10288
+ * the eligibility IO entirely. The decision remains heavily gated — see
10289
+ * `./codex-auto-reset` and the design in `local://autoreset-spec.md`.
10290
+ * Per-account in-flight dedup lets concurrent sessions adopt one redeem
10291
+ * instead of double-spending.
10292
+ */
10293
+ async #maybeAutoRedeemCodexReset(coordinator = defaultCodexAutoRedeemCoordinator): Promise<boolean> {
10294
+ const cfg = this.settings.getGroup("codexResets");
10295
+ const model = this.model;
10296
+ // Cheap exits before any IO.
10297
+ if (!shouldEvaluateCodexAutoRedeem(cfg.autoRedeem) || !model || model.provider !== "openai-codex") return false;
10298
+ const authStorage = this.#modelRegistry.authStorage;
10299
+ // Capture identity BEFORE awaits: markUsageLimitReached leaves the
10300
+ // usage-limit session credential sticky, so this names the blocked account.
10301
+ const identity = authStorage.getOAuthAccountIdentity("openai-codex", this.sessionId);
10302
+ const accountKey = (identity?.accountId ?? identity?.email)?.trim().toLowerCase();
10303
+ if (!accountKey) return false;
10304
+ const existing = coordinator.inFlightByAccount.get(accountKey);
10305
+ if (existing) return existing;
10306
+
10307
+ const run = (async (): Promise<boolean> => {
10308
+ const reports = await this.fetchUsageReports();
10309
+ const decision = evaluateCodexAutoRedeem({
10310
+ nowMs: Date.now(),
10311
+ provider: model.provider,
10312
+ modelId: model.id,
10313
+ settings: {
10314
+ autoRedeem: true,
10315
+ minBlockedMinutes: Math.max(0, cfg.minBlockedMinutes),
10316
+ keepCredits: Math.max(0, Math.trunc(cfg.keepCredits)),
10317
+ },
10318
+ identity,
10319
+ reports,
10320
+ attemptedBlockKeys: coordinator.attemptedBlockKeys,
10321
+ lastAttemptAtByAccount: coordinator.lastAttemptAtByAccount,
10322
+ });
10323
+ if (!decision.redeem) {
10324
+ logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
10325
+ return false;
10326
+ }
10327
+ if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
10328
+ return false;
10329
+ }
10330
+ // Commit the attempt BEFORE acting so this block can never re-enter.
10331
+ coordinator.attemptedBlockKeys.add(decision.blockKey);
10332
+ coordinator.lastAttemptAtByAccount.set(decision.accountKey, Date.now());
10333
+ const who = decision.target.email ?? decision.target.accountId ?? "the active account";
10334
+ const outcome = await authStorage.redeemResetCredit({
10335
+ target: decision.target,
10336
+ baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
10337
+ // Not tied to the retry abort controller: aborting a consume
10338
+ // mid-flight leaves credit state unknown.
10339
+ signal: AbortSignal.timeout(15_000),
10340
+ });
10341
+ switch (outcome.code) {
10342
+ case "reset": {
10343
+ const left = Math.max(0, decision.availableCount - 1);
10344
+ this.emitNotice(
10345
+ "info",
10346
+ `Auto-redeemed a saved Codex rate-limit reset for ${who} (${left} left); retrying now.`,
10347
+ "codex-auto-reset",
10348
+ );
10349
+ void this.fetchUsageReports();
10350
+ return true;
10351
+ }
10352
+ case "already_redeemed":
10353
+ this.emitNotice(
10354
+ "warning",
10355
+ "A saved Codex reset was already redeemed elsewhere; waiting for the window.",
10356
+ "codex-auto-reset",
10357
+ );
10358
+ return false;
10359
+ case "no_credit":
10360
+ logger.debug("codex-auto-reset: no_credit (snapshot/live mismatch)", { account: accountKey });
10361
+ return false;
10362
+ case "nothing_to_reset":
10363
+ this.emitNotice(
10364
+ "warning",
10365
+ "Codex reset reported nothing to reset; auto-redeem suppressed for this window.",
10366
+ "codex-auto-reset",
10367
+ );
10368
+ return false;
10369
+ default:
10370
+ this.emitNotice("warning", `Codex auto-redeem failed (${outcome.code}).`, "codex-auto-reset");
10371
+ return false;
10372
+ }
10373
+ })().finally(() => coordinator.inFlightByAccount.delete(accountKey));
10374
+ coordinator.inFlightByAccount.set(accountKey, run);
10375
+ return run;
10376
+ }
10377
+
9545
10378
  /**
9546
10379
  * Estimate context tokens from messages, using the last assistant usage when available.
9547
10380
  */
@@ -9594,6 +10427,7 @@ export class AgentSession {
9594
10427
  */
9595
10428
  async exportToHtml(outputPath?: string): Promise<string> {
9596
10429
  const themeName = getCurrentThemeName();
10430
+ const { exportSessionToHtml } = await import("../export/html");
9597
10431
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
9598
10432
  }
9599
10433
 
@@ -9682,69 +10516,6 @@ export class AgentSession {
9682
10516
  });
9683
10517
  }
9684
10518
 
9685
- /**
9686
- * Format the conversation as compact context for subagents.
9687
- * Includes only user messages and assistant text responses.
9688
- * Excludes: system prompt, tool definitions, tool calls/results, thinking blocks.
9689
- */
9690
- formatCompactContext(): string {
9691
- const lines: string[] = [];
9692
- lines.push("# Conversation Context");
9693
- lines.push("");
9694
- lines.push(
9695
- "This is a summary of the parent conversation. Read this if you need additional context about what was discussed or decided.",
9696
- );
9697
- lines.push("");
9698
-
9699
- for (const msg of this.messages) {
9700
- if (msg.role === "user" || msg.role === "developer") {
9701
- lines.push(msg.role === "developer" ? "## Developer" : "## User");
9702
- lines.push("");
9703
- if (typeof msg.content === "string") {
9704
- lines.push(msg.content);
9705
- } else {
9706
- for (const c of msg.content) {
9707
- if (c.type === "text") {
9708
- lines.push(c.text);
9709
- } else if (c.type === "image") {
9710
- lines.push("[Image attached]");
9711
- }
9712
- }
9713
- }
9714
- lines.push("");
9715
- } else if (msg.role === "assistant") {
9716
- const assistantMsg = msg as AssistantMessage;
9717
- // Only include text content, skip tool calls and thinking
9718
- const textParts: string[] = [];
9719
- for (const c of assistantMsg.content) {
9720
- if (c.type === "text" && c.text.trim()) {
9721
- textParts.push(c.text);
9722
- }
9723
- }
9724
- if (textParts.length > 0) {
9725
- lines.push("## Assistant");
9726
- lines.push("");
9727
- lines.push(textParts.join("\n\n"));
9728
- lines.push("");
9729
- }
9730
- } else if (msg.role === "fileMention") {
9731
- const fileMsg = msg as FileMentionMessage;
9732
- const paths = fileMsg.files.map(f => f.path).join(", ");
9733
- lines.push(`[Files referenced: ${paths}]`);
9734
- lines.push("");
9735
- } else if (msg.role === "compactionSummary") {
9736
- const compactMsg = msg as CompactionSummaryMessage;
9737
- lines.push("## Earlier Context (Summarized)");
9738
- lines.push("");
9739
- lines.push(compactMsg.summary);
9740
- lines.push("");
9741
- }
9742
- // Skip: toolResult, bashExecution, pythonExecution, branchSummary, custom, hookMessage
9743
- }
9744
-
9745
- return lines.join("\n").trim();
9746
- }
9747
-
9748
10519
  // =========================================================================
9749
10520
  // Extension System
9750
10521
  // =========================================================================