@kr78/pi-coding-agent 17.2.10

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 (1593) hide show
  1. package/CHANGELOG.md +14861 -0
  2. package/README.md +35 -0
  3. package/dist/CHANGELOG-chtdxh1z.md +14861 -0
  4. package/dist/cli.js +20483 -0
  5. package/dist/template-c2hyaytt.js +1653 -0
  6. package/dist/template-f8wx9vfn.css +1355 -0
  7. package/dist/template-qat058wr.html +55 -0
  8. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  9. package/examples/README.md +21 -0
  10. package/examples/custom-tools/README.md +104 -0
  11. package/examples/custom-tools/hello/index.ts +20 -0
  12. package/examples/extensions/README.md +142 -0
  13. package/examples/extensions/api-demo.ts +78 -0
  14. package/examples/extensions/chalk-logger.ts +25 -0
  15. package/examples/extensions/hello.ts +31 -0
  16. package/examples/extensions/pirate.ts +43 -0
  17. package/examples/extensions/plan-mode.ts +549 -0
  18. package/examples/extensions/reload-runtime.ts +38 -0
  19. package/examples/extensions/thinking-note.ts +13 -0
  20. package/examples/extensions/tools.ts +145 -0
  21. package/examples/extensions/with-deps/index.ts +36 -0
  22. package/examples/extensions/with-deps/package-lock.json +31 -0
  23. package/examples/extensions/with-deps/package.json +17 -0
  24. package/examples/hooks/README.md +56 -0
  25. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  26. package/examples/hooks/confirm-destructive.ts +58 -0
  27. package/examples/hooks/custom-compaction.ts +115 -0
  28. package/examples/hooks/dirty-repo-guard.ts +51 -0
  29. package/examples/hooks/file-trigger.ts +40 -0
  30. package/examples/hooks/git-checkpoint.ts +52 -0
  31. package/examples/hooks/handoff.ts +149 -0
  32. package/examples/hooks/permission-gate.ts +33 -0
  33. package/examples/hooks/protected-paths.ts +29 -0
  34. package/examples/hooks/qna.ts +118 -0
  35. package/examples/hooks/status-line.ts +39 -0
  36. package/examples/sdk/01-minimal.ts +21 -0
  37. package/examples/sdk/02-custom-model.ts +49 -0
  38. package/examples/sdk/03-custom-prompt.ts +46 -0
  39. package/examples/sdk/04-skills.ts +43 -0
  40. package/examples/sdk/06-extensions.ts +82 -0
  41. package/examples/sdk/06-hooks.ts +61 -0
  42. package/examples/sdk/07-context-files.ts +35 -0
  43. package/examples/sdk/08-prompt-templates.ts +41 -0
  44. package/examples/sdk/08-slash-commands.ts +46 -0
  45. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  46. package/examples/sdk/11-sessions.ts +47 -0
  47. package/examples/sdk/12-redis-sessions.ts +54 -0
  48. package/examples/sdk/13-sql-sessions.ts +61 -0
  49. package/examples/sdk/README.md +169 -0
  50. package/package.json +587 -0
  51. package/scripts/bench-guard.ts +71 -0
  52. package/scripts/bench-title-models.ts +332 -0
  53. package/scripts/build-binary.ts +117 -0
  54. package/scripts/bundle-dist.ts +123 -0
  55. package/scripts/compile-binary.ts +69 -0
  56. package/scripts/embed-mupdf-wasm.ts +67 -0
  57. package/scripts/format-prompts.ts +68 -0
  58. package/scripts/generate-aria-snapshot.ts +134 -0
  59. package/scripts/generate-docs-index.ts +58 -0
  60. package/scripts/generate-share-viewer.ts +34 -0
  61. package/scripts/legacy-pi-virtual-module.ts +205 -0
  62. package/scripts/measure-prompt-tokens.ts +63 -0
  63. package/scripts/omp +51 -0
  64. package/scripts/omp.ts +19 -0
  65. package/scripts/security-compare.ts +40 -0
  66. package/src/advisor/advise-tool.ts +234 -0
  67. package/src/advisor/config.ts +341 -0
  68. package/src/advisor/emission-guard.ts +172 -0
  69. package/src/advisor/index.ts +6 -0
  70. package/src/advisor/runtime.ts +1412 -0
  71. package/src/advisor/transcript-recorder.ts +215 -0
  72. package/src/advisor/watchdog.ts +135 -0
  73. package/src/async/index.ts +1 -0
  74. package/src/async/job-manager.ts +847 -0
  75. package/src/auto-thinking/classifier.ts +220 -0
  76. package/src/autolearn/controller.ts +152 -0
  77. package/src/autolearn/managed-skills.ts +255 -0
  78. package/src/autoresearch/command-resume.md +14 -0
  79. package/src/autoresearch/dashboard.ts +436 -0
  80. package/src/autoresearch/git.ts +331 -0
  81. package/src/autoresearch/helpers.ts +218 -0
  82. package/src/autoresearch/index.ts +541 -0
  83. package/src/autoresearch/prompt-setup.md +43 -0
  84. package/src/autoresearch/prompt.md +103 -0
  85. package/src/autoresearch/resume-message.md +10 -0
  86. package/src/autoresearch/state.ts +273 -0
  87. package/src/autoresearch/storage.ts +700 -0
  88. package/src/autoresearch/tools/init-experiment.ts +268 -0
  89. package/src/autoresearch/tools/log-experiment.ts +520 -0
  90. package/src/autoresearch/tools/run-experiment.ts +407 -0
  91. package/src/autoresearch/tools/update-notes.ts +109 -0
  92. package/src/autoresearch/types.ts +168 -0
  93. package/src/capability/context-file.ts +44 -0
  94. package/src/capability/extension-module.ts +34 -0
  95. package/src/capability/extension.ts +47 -0
  96. package/src/capability/fs.ts +117 -0
  97. package/src/capability/hook.ts +40 -0
  98. package/src/capability/index.ts +467 -0
  99. package/src/capability/instruction.ts +37 -0
  100. package/src/capability/mcp.ts +115 -0
  101. package/src/capability/prompt.ts +35 -0
  102. package/src/capability/rule-buckets.ts +66 -0
  103. package/src/capability/rule.ts +298 -0
  104. package/src/capability/settings.ts +34 -0
  105. package/src/capability/skill.ts +69 -0
  106. package/src/capability/slash-command.ts +40 -0
  107. package/src/capability/ssh.ts +41 -0
  108. package/src/capability/system-prompt.ts +34 -0
  109. package/src/capability/tool.ts +38 -0
  110. package/src/capability/types.ts +187 -0
  111. package/src/cleanse/agent.ts +226 -0
  112. package/src/cleanse/balance.ts +79 -0
  113. package/src/cleanse/checkers.ts +996 -0
  114. package/src/cleanse/index.ts +190 -0
  115. package/src/cleanse/loop.ts +51 -0
  116. package/src/cleanse/parsers.ts +726 -0
  117. package/src/cleanse/progress.ts +50 -0
  118. package/src/cleanse/prompts/assignment.md +47 -0
  119. package/src/cleanse/types.ts +72 -0
  120. package/src/cli/agents-cli.ts +138 -0
  121. package/src/cli/args.ts +368 -0
  122. package/src/cli/auth-broker-cli.ts +940 -0
  123. package/src/cli/auth-gateway-cli.ts +674 -0
  124. package/src/cli/bench-cli.ts +990 -0
  125. package/src/cli/browser-relay-cli.ts +119 -0
  126. package/src/cli/classify-install-target.ts +76 -0
  127. package/src/cli/claude-trace-cli.ts +795 -0
  128. package/src/cli/command-help.ts +107 -0
  129. package/src/cli/commands/init-xdg.ts +27 -0
  130. package/src/cli/completion-gen.ts +550 -0
  131. package/src/cli/config-cli.ts +459 -0
  132. package/src/cli/dry-balance-cli.ts +864 -0
  133. package/src/cli/extension-flags.ts +48 -0
  134. package/src/cli/file-processor.ts +132 -0
  135. package/src/cli/flag-tables.ts +367 -0
  136. package/src/cli/gallery-cli.ts +272 -0
  137. package/src/cli/gallery-fixtures/agentic.ts +420 -0
  138. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  139. package/src/cli/gallery-fixtures/edit.ts +254 -0
  140. package/src/cli/gallery-fixtures/fs.ts +245 -0
  141. package/src/cli/gallery-fixtures/index.ts +40 -0
  142. package/src/cli/gallery-fixtures/interaction.ts +46 -0
  143. package/src/cli/gallery-fixtures/memory.ts +81 -0
  144. package/src/cli/gallery-fixtures/misc.ts +177 -0
  145. package/src/cli/gallery-fixtures/search.ts +135 -0
  146. package/src/cli/gallery-fixtures/shell.ts +241 -0
  147. package/src/cli/gallery-fixtures/types.ts +57 -0
  148. package/src/cli/gallery-fixtures/web.ts +158 -0
  149. package/src/cli/gallery-screenshot.ts +279 -0
  150. package/src/cli/gc-cli.ts +1566 -0
  151. package/src/cli/grep-cli.ts +161 -0
  152. package/src/cli/grievances-cli.ts +256 -0
  153. package/src/cli/help-extra.ts +89 -0
  154. package/src/cli/initial-message.ts +58 -0
  155. package/src/cli/models-cli.ts +385 -0
  156. package/src/cli/plugin-cli.ts +996 -0
  157. package/src/cli/profile-alias.ts +369 -0
  158. package/src/cli/profile-bootstrap.ts +233 -0
  159. package/src/cli/read-cli.ts +99 -0
  160. package/src/cli/session-picker.ts +110 -0
  161. package/src/cli/setup-cli.ts +312 -0
  162. package/src/cli/setup-model-picker.ts +43 -0
  163. package/src/cli/shell-cli.ts +176 -0
  164. package/src/cli/ssh-cli.ts +179 -0
  165. package/src/cli/startup-cwd.ts +58 -0
  166. package/src/cli/stats-cli.ts +229 -0
  167. package/src/cli/thinking-levels.ts +7 -0
  168. package/src/cli/tiny-models-cli.ts +153 -0
  169. package/src/cli/ttsr-cli.ts +1013 -0
  170. package/src/cli/update-cli.ts +1184 -0
  171. package/src/cli/usage-cli.ts +1084 -0
  172. package/src/cli/usage-error.ts +7 -0
  173. package/src/cli/web-search-cli.ts +144 -0
  174. package/src/cli/worktree-cli.ts +311 -0
  175. package/src/cli-commands.ts +308 -0
  176. package/src/cli.ts +428 -0
  177. package/src/collab/crypto.ts +63 -0
  178. package/src/collab/display-name.ts +13 -0
  179. package/src/collab/guest.ts +764 -0
  180. package/src/collab/host.ts +692 -0
  181. package/src/collab/protocol.ts +296 -0
  182. package/src/collab/relay-client.ts +282 -0
  183. package/src/collab/replication-shrink.ts +111 -0
  184. package/src/commands/acp.ts +35 -0
  185. package/src/commands/agents.ts +58 -0
  186. package/src/commands/auth-broker.ts +100 -0
  187. package/src/commands/auth-gateway.ts +70 -0
  188. package/src/commands/bench.ts +64 -0
  189. package/src/commands/browser-relay.ts +53 -0
  190. package/src/commands/cleanse.ts +45 -0
  191. package/src/commands/commit.ts +47 -0
  192. package/src/commands/complete.ts +67 -0
  193. package/src/commands/completions.ts +61 -0
  194. package/src/commands/config.ts +52 -0
  195. package/src/commands/dry-balance.ts +43 -0
  196. package/src/commands/gallery.ts +61 -0
  197. package/src/commands/gc.ts +47 -0
  198. package/src/commands/grep.ts +49 -0
  199. package/src/commands/grievances.ts +52 -0
  200. package/src/commands/install.ts +107 -0
  201. package/src/commands/join.ts +40 -0
  202. package/src/commands/launch-help.ts +116 -0
  203. package/src/commands/launch.ts +34 -0
  204. package/src/commands/models.ts +61 -0
  205. package/src/commands/plugin.ts +79 -0
  206. package/src/commands/read.ts +39 -0
  207. package/src/commands/say.ts +146 -0
  208. package/src/commands/setup.ts +68 -0
  209. package/src/commands/share.ts +71 -0
  210. package/src/commands/shell.ts +30 -0
  211. package/src/commands/ssh.ts +61 -0
  212. package/src/commands/stats.ts +30 -0
  213. package/src/commands/tiny-models.ts +36 -0
  214. package/src/commands/token.ts +164 -0
  215. package/src/commands/ttsr.ts +125 -0
  216. package/src/commands/update.ts +34 -0
  217. package/src/commands/usage.ts +55 -0
  218. package/src/commands/web-search.ts +43 -0
  219. package/src/commands/worktree.ts +63 -0
  220. package/src/commit/agentic/agent.ts +322 -0
  221. package/src/commit/agentic/fallback.ts +96 -0
  222. package/src/commit/agentic/index.ts +383 -0
  223. package/src/commit/agentic/lock-files.ts +107 -0
  224. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  225. package/src/commit/agentic/prompts/session-user.md +25 -0
  226. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  227. package/src/commit/agentic/prompts/system.md +38 -0
  228. package/src/commit/agentic/state.ts +60 -0
  229. package/src/commit/agentic/tools/analyze-file.ts +148 -0
  230. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  231. package/src/commit/agentic/tools/git-hunk.ts +52 -0
  232. package/src/commit/agentic/tools/git-overview.ts +62 -0
  233. package/src/commit/agentic/tools/index.ts +54 -0
  234. package/src/commit/agentic/tools/propose-changelog.ts +147 -0
  235. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  236. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  237. package/src/commit/agentic/tools/schemas.ts +11 -0
  238. package/src/commit/agentic/tools/split-commit.ts +241 -0
  239. package/src/commit/agentic/topo-sort.ts +44 -0
  240. package/src/commit/agentic/trivial.ts +51 -0
  241. package/src/commit/agentic/validation.ts +183 -0
  242. package/src/commit/analysis/conventional.ts +64 -0
  243. package/src/commit/analysis/index.ts +4 -0
  244. package/src/commit/analysis/scope.ts +242 -0
  245. package/src/commit/analysis/summary.ts +107 -0
  246. package/src/commit/analysis/validation.ts +66 -0
  247. package/src/commit/changelog/detect.ts +40 -0
  248. package/src/commit/changelog/generate.ts +101 -0
  249. package/src/commit/changelog/index.ts +234 -0
  250. package/src/commit/changelog/parse.ts +44 -0
  251. package/src/commit/cli.ts +85 -0
  252. package/src/commit/git/diff.ts +148 -0
  253. package/src/commit/index.ts +5 -0
  254. package/src/commit/map-reduce/index.ts +69 -0
  255. package/src/commit/map-reduce/map-phase.ts +193 -0
  256. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  257. package/src/commit/map-reduce/utils.ts +9 -0
  258. package/src/commit/message.ts +11 -0
  259. package/src/commit/model-selection.ts +95 -0
  260. package/src/commit/pipeline.ts +244 -0
  261. package/src/commit/prompts/analysis-system.md +148 -0
  262. package/src/commit/prompts/analysis-user.md +38 -0
  263. package/src/commit/prompts/changelog-system.md +50 -0
  264. package/src/commit/prompts/changelog-user.md +18 -0
  265. package/src/commit/prompts/file-observer-system.md +24 -0
  266. package/src/commit/prompts/file-observer-user.md +8 -0
  267. package/src/commit/prompts/reduce-system.md +50 -0
  268. package/src/commit/prompts/reduce-user.md +17 -0
  269. package/src/commit/prompts/summary-retry.md +3 -0
  270. package/src/commit/prompts/summary-system.md +38 -0
  271. package/src/commit/prompts/summary-user.md +13 -0
  272. package/src/commit/prompts/types-description.md +2 -0
  273. package/src/commit/shared-llm.ts +70 -0
  274. package/src/commit/types.ts +118 -0
  275. package/src/commit/utils/exclusions.ts +42 -0
  276. package/src/commit/utils.ts +58 -0
  277. package/src/config/api-key-resolver.ts +81 -0
  278. package/src/config/append-only-context-mode.ts +76 -0
  279. package/src/config/config-file.ts +347 -0
  280. package/src/config/inline-tool-descriptors-mode.ts +26 -0
  281. package/src/config/keybindings.ts +703 -0
  282. package/src/config/mcp-schema.json +252 -0
  283. package/src/config/model-discovery.ts +1077 -0
  284. package/src/config/model-registry.ts +2822 -0
  285. package/src/config/model-resolver.ts +2115 -0
  286. package/src/config/model-roles.ts +113 -0
  287. package/src/config/models-config-schema-bundle.ts +315 -0
  288. package/src/config/models-config-schema.ts +14 -0
  289. package/src/config/models-config.ts +130 -0
  290. package/src/config/prompt-templates.ts +205 -0
  291. package/src/config/provider-globals.ts +25 -0
  292. package/src/config/resolve-config-value.ts +95 -0
  293. package/src/config/service-tier.ts +146 -0
  294. package/src/config/settings-schema.ts +5887 -0
  295. package/src/config/settings.ts +2394 -0
  296. package/src/config.ts +242 -0
  297. package/src/cursor-bridge-tools.ts +81 -0
  298. package/src/cursor.ts +960 -0
  299. package/src/dap/client.ts +1043 -0
  300. package/src/dap/config.ts +480 -0
  301. package/src/dap/defaults.json +212 -0
  302. package/src/dap/index.ts +4 -0
  303. package/src/dap/session.ts +1841 -0
  304. package/src/dap/types.ts +611 -0
  305. package/src/debug/index.ts +584 -0
  306. package/src/debug/log-formatting.ts +58 -0
  307. package/src/debug/log-viewer.ts +966 -0
  308. package/src/debug/profiler.ts +168 -0
  309. package/src/debug/protocol-probe.ts +267 -0
  310. package/src/debug/raw-sse-buffer.ts +421 -0
  311. package/src/debug/raw-sse.ts +312 -0
  312. package/src/debug/remote-debugger.ts +151 -0
  313. package/src/debug/report-bundle.ts +411 -0
  314. package/src/debug/system-info.ts +111 -0
  315. package/src/debug/terminal-info.ts +124 -0
  316. package/src/discovery/agent-plugin-format.ts +551 -0
  317. package/src/discovery/agent-plugins.ts +341 -0
  318. package/src/discovery/agents-md.ts +67 -0
  319. package/src/discovery/agents.ts +300 -0
  320. package/src/discovery/at-imports.ts +273 -0
  321. package/src/discovery/builtin-defaults.ts +39 -0
  322. package/src/discovery/builtin-rules/go-add-cleanup.md +33 -0
  323. package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
  324. package/src/discovery/builtin-rules/go-exp-promoted.md +40 -0
  325. package/src/discovery/builtin-rules/go-ioutil.md +37 -0
  326. package/src/discovery/builtin-rules/go-join-hostport.md +30 -0
  327. package/src/discovery/builtin-rules/go-new-expr.md +44 -0
  328. package/src/discovery/builtin-rules/go-rand-v2.md +41 -0
  329. package/src/discovery/builtin-rules/go-range-int.md +45 -0
  330. package/src/discovery/builtin-rules/index.ts +74 -0
  331. package/src/discovery/builtin-rules/rs-box-leak.md +49 -0
  332. package/src/discovery/builtin-rules/rs-future-prelude.md +24 -0
  333. package/src/discovery/builtin-rules/rs-lazylock.md +52 -0
  334. package/src/discovery/builtin-rules/rs-match-ergonomics.md +68 -0
  335. package/src/discovery/builtin-rules/rs-parking-lot.md +45 -0
  336. package/src/discovery/builtin-rules/rs-result-type.md +20 -0
  337. package/src/discovery/builtin-rules/ts-bare-catch.md +39 -0
  338. package/src/discovery/builtin-rules/ts-import-type.md +43 -0
  339. package/src/discovery/builtin-rules/ts-no-any.md +66 -0
  340. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +45 -0
  341. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +40 -0
  342. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +56 -0
  343. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  344. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  345. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  346. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  347. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +66 -0
  348. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  349. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  350. package/src/discovery/builtin.ts +945 -0
  351. package/src/discovery/claude-plugins.ts +640 -0
  352. package/src/discovery/claude.ts +591 -0
  353. package/src/discovery/cline.ts +83 -0
  354. package/src/discovery/codex.ts +553 -0
  355. package/src/discovery/contained-path.ts +78 -0
  356. package/src/discovery/cursor.ts +223 -0
  357. package/src/discovery/gemini.ts +386 -0
  358. package/src/discovery/github.ts +337 -0
  359. package/src/discovery/helpers.ts +1183 -0
  360. package/src/discovery/index.ts +82 -0
  361. package/src/discovery/mcp-json.ts +182 -0
  362. package/src/discovery/omp-extension-roots.ts +272 -0
  363. package/src/discovery/omp-plugins.ts +409 -0
  364. package/src/discovery/opencode.ts +441 -0
  365. package/src/discovery/plugin-dir-roots.ts +28 -0
  366. package/src/discovery/ssh.ts +153 -0
  367. package/src/discovery/substitute-plugin-root.ts +77 -0
  368. package/src/discovery/vscode.ts +106 -0
  369. package/src/discovery/windsurf.ts +149 -0
  370. package/src/edit/apply-patch/index.ts +87 -0
  371. package/src/edit/apply-patch/parser.ts +174 -0
  372. package/src/edit/diff.ts +1008 -0
  373. package/src/edit/edit-clipboard.ts +23 -0
  374. package/src/edit/file-snapshot-store.ts +148 -0
  375. package/src/edit/hashline/block-resolver.ts +33 -0
  376. package/src/edit/hashline/diff.ts +402 -0
  377. package/src/edit/hashline/execute.ts +313 -0
  378. package/src/edit/hashline/filesystem.ts +247 -0
  379. package/src/edit/hashline/index.ts +5 -0
  380. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  381. package/src/edit/hashline/params.ts +12 -0
  382. package/src/edit/index.ts +699 -0
  383. package/src/edit/modes/apply-patch.lark +19 -0
  384. package/src/edit/modes/apply-patch.ts +53 -0
  385. package/src/edit/modes/patch.ts +1958 -0
  386. package/src/edit/modes/replace.ts +1191 -0
  387. package/src/edit/normalize.ts +345 -0
  388. package/src/edit/notebook.ts +242 -0
  389. package/src/edit/read-file.ts +25 -0
  390. package/src/edit/renderer.ts +998 -0
  391. package/src/edit/snapshot-details.ts +77 -0
  392. package/src/edit/streaming.ts +710 -0
  393. package/src/eval/agent-bridge.ts +224 -0
  394. package/src/eval/backend-helpers.ts +48 -0
  395. package/src/eval/backend.ts +71 -0
  396. package/src/eval/bridge-timeout.ts +65 -0
  397. package/src/eval/budget-bridge.ts +48 -0
  398. package/src/eval/completion-bridge.ts +212 -0
  399. package/src/eval/concurrency-bridge.ts +34 -0
  400. package/src/eval/executor-base.ts +562 -0
  401. package/src/eval/idle-timeout.ts +91 -0
  402. package/src/eval/index.ts +6 -0
  403. package/src/eval/jl/executor.ts +551 -0
  404. package/src/eval/jl/index.ts +54 -0
  405. package/src/eval/jl/kernel.ts +236 -0
  406. package/src/eval/jl/prelude.jl +736 -0
  407. package/src/eval/jl/prelude.ts +3 -0
  408. package/src/eval/jl/runner.jl +666 -0
  409. package/src/eval/jl/runtime.ts +118 -0
  410. package/src/eval/js/context-manager.ts +872 -0
  411. package/src/eval/js/executor.ts +178 -0
  412. package/src/eval/js/index.ts +41 -0
  413. package/src/eval/js/process-entry.ts +31 -0
  414. package/src/eval/js/shared/helpers.ts +170 -0
  415. package/src/eval/js/shared/indirect-eval.ts +30 -0
  416. package/src/eval/js/shared/local-module-loader.ts +364 -0
  417. package/src/eval/js/shared/prelude.ts +2 -0
  418. package/src/eval/js/shared/prelude.txt +294 -0
  419. package/src/eval/js/shared/rewrite-imports.ts +550 -0
  420. package/src/eval/js/shared/runtime.ts +590 -0
  421. package/src/eval/js/shared/types.ts +18 -0
  422. package/src/eval/js/tool-bridge.ts +163 -0
  423. package/src/eval/js/worker-core.ts +380 -0
  424. package/src/eval/js/worker-entry.ts +37 -0
  425. package/src/eval/js/worker-protocol.ts +47 -0
  426. package/src/eval/kernel-base.ts +569 -0
  427. package/src/eval/py/display.ts +71 -0
  428. package/src/eval/py/executor.ts +643 -0
  429. package/src/eval/py/index.ts +57 -0
  430. package/src/eval/py/kernel.ts +235 -0
  431. package/src/eval/py/prelude.py +673 -0
  432. package/src/eval/py/prelude.ts +3 -0
  433. package/src/eval/py/runner.py +1425 -0
  434. package/src/eval/py/runtime.ts +276 -0
  435. package/src/eval/py/spawn-options.ts +134 -0
  436. package/src/eval/py/tool-bridge.ts +201 -0
  437. package/src/eval/rb/executor.ts +511 -0
  438. package/src/eval/rb/index.ts +54 -0
  439. package/src/eval/rb/kernel.ts +231 -0
  440. package/src/eval/rb/prelude.rb +551 -0
  441. package/src/eval/rb/prelude.ts +3 -0
  442. package/src/eval/rb/runner.rb +581 -0
  443. package/src/eval/rb/runtime.ts +132 -0
  444. package/src/eval/runtime-env.ts +104 -0
  445. package/src/eval/session-id.ts +8 -0
  446. package/src/eval/types.ts +48 -0
  447. package/src/exa/index.ts +2 -0
  448. package/src/exa/mcp-client.ts +370 -0
  449. package/src/exa/types.ts +69 -0
  450. package/src/exec/bash-executor.ts +627 -0
  451. package/src/exec/direnv.ts +145 -0
  452. package/src/exec/exec.ts +53 -0
  453. package/src/exec/non-interactive-env.ts +118 -0
  454. package/src/export/custom-share.ts +65 -0
  455. package/src/export/html/args.ts +20 -0
  456. package/src/export/html/index.ts +320 -0
  457. package/src/export/html/share-loader.js +102 -0
  458. package/src/export/html/template.css +1355 -0
  459. package/src/export/html/template.html +55 -0
  460. package/src/export/html/template.js +1653 -0
  461. package/src/export/html/tool-views.generated.js +35 -0
  462. package/src/export/html/vendor/highlight.min.js +1213 -0
  463. package/src/export/html/vendor/marked.min.js +6 -0
  464. package/src/export/html/web-palette.ts +126 -0
  465. package/src/export/share.ts +686 -0
  466. package/src/export/ttsr.ts +590 -0
  467. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  468. package/src/extensibility/custom-commands/bundled/review/index.ts +698 -0
  469. package/src/extensibility/custom-commands/index.ts +2 -0
  470. package/src/extensibility/custom-commands/loader.ts +242 -0
  471. package/src/extensibility/custom-commands/types.ts +119 -0
  472. package/src/extensibility/custom-tools/index.ts +7 -0
  473. package/src/extensibility/custom-tools/loader.ts +301 -0
  474. package/src/extensibility/custom-tools/types.ts +287 -0
  475. package/src/extensibility/custom-tools/wrapper.ts +50 -0
  476. package/src/extensibility/extensions/compact-handler.ts +40 -0
  477. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  478. package/src/extensibility/extensions/index.ts +16 -0
  479. package/src/extensibility/extensions/load-errors.ts +13 -0
  480. package/src/extensibility/extensions/loader.ts +663 -0
  481. package/src/extensibility/extensions/managed-timers.ts +83 -0
  482. package/src/extensibility/extensions/model-api.ts +39 -0
  483. package/src/extensibility/extensions/runner.ts +1396 -0
  484. package/src/extensibility/extensions/types.ts +1603 -0
  485. package/src/extensibility/extensions/wrapper.ts +395 -0
  486. package/src/extensibility/hooks/index.ts +6 -0
  487. package/src/extensibility/hooks/loader.ts +243 -0
  488. package/src/extensibility/hooks/runner.ts +425 -0
  489. package/src/extensibility/hooks/tool-wrapper.ts +124 -0
  490. package/src/extensibility/hooks/types.ts +612 -0
  491. package/src/extensibility/legacy-pi-ai-shim.ts +161 -0
  492. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1460 -0
  493. package/src/extensibility/legacy-pi-tui-shim.ts +43 -0
  494. package/src/extensibility/legacy-typebox.ts +101 -0
  495. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  496. package/src/extensibility/plugins/doctor.ts +65 -0
  497. package/src/extensibility/plugins/git-url.ts +367 -0
  498. package/src/extensibility/plugins/index.ts +9 -0
  499. package/src/extensibility/plugins/installer.ts +201 -0
  500. package/src/extensibility/plugins/legacy-pi-compat.ts +2643 -0
  501. package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
  502. package/src/extensibility/plugins/loader.ts +535 -0
  503. package/src/extensibility/plugins/manager.ts +1142 -0
  504. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  505. package/src/extensibility/plugins/marketplace/fetcher.ts +316 -0
  506. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  507. package/src/extensibility/plugins/marketplace/manager.ts +927 -0
  508. package/src/extensibility/plugins/marketplace/registry.ts +192 -0
  509. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  510. package/src/extensibility/plugins/marketplace/types.ts +192 -0
  511. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  512. package/src/extensibility/plugins/parser.ts +107 -0
  513. package/src/extensibility/plugins/runtime-config.ts +9 -0
  514. package/src/extensibility/plugins/types.ts +194 -0
  515. package/src/extensibility/session-handler-types.ts +21 -0
  516. package/src/extensibility/shared-events.ts +401 -0
  517. package/src/extensibility/skills.ts +531 -0
  518. package/src/extensibility/slash-commands.ts +131 -0
  519. package/src/extensibility/tool-event-input.ts +80 -0
  520. package/src/extensibility/tool-proxy.ts +28 -0
  521. package/src/extensibility/utils.ts +192 -0
  522. package/src/goals/index.ts +3 -0
  523. package/src/goals/runtime.ts +521 -0
  524. package/src/goals/state.ts +37 -0
  525. package/src/goals/tools/goal-tool.ts +251 -0
  526. package/src/hindsight/backend.ts +354 -0
  527. package/src/hindsight/bank.ts +156 -0
  528. package/src/hindsight/client.ts +680 -0
  529. package/src/hindsight/config.ts +193 -0
  530. package/src/hindsight/content.ts +266 -0
  531. package/src/hindsight/index.ts +8 -0
  532. package/src/hindsight/mental-models.ts +429 -0
  533. package/src/hindsight/seeds.json +32 -0
  534. package/src/hindsight/state.ts +530 -0
  535. package/src/hindsight/transcript.ts +71 -0
  536. package/src/index.ts +66 -0
  537. package/src/internal-urls/agent-protocol.ts +180 -0
  538. package/src/internal-urls/artifact-protocol.ts +151 -0
  539. package/src/internal-urls/docs-index.ts +102 -0
  540. package/src/internal-urls/filesystem-resource.ts +34 -0
  541. package/src/internal-urls/history-protocol.ts +198 -0
  542. package/src/internal-urls/index.ts +28 -0
  543. package/src/internal-urls/issue-pr-protocol.ts +594 -0
  544. package/src/internal-urls/json-query.ts +126 -0
  545. package/src/internal-urls/local-protocol.ts +471 -0
  546. package/src/internal-urls/mcp-protocol.ts +168 -0
  547. package/src/internal-urls/memory-protocol.ts +380 -0
  548. package/src/internal-urls/omp-protocol.ts +94 -0
  549. package/src/internal-urls/parse.ts +103 -0
  550. package/src/internal-urls/registry-helpers.ts +132 -0
  551. package/src/internal-urls/router.ts +153 -0
  552. package/src/internal-urls/rule-protocol.ts +45 -0
  553. package/src/internal-urls/security-protocol.ts +261 -0
  554. package/src/internal-urls/skill-protocol.ts +131 -0
  555. package/src/internal-urls/ssh-protocol.ts +368 -0
  556. package/src/internal-urls/types.ts +196 -0
  557. package/src/internal-urls/vault-protocol.ts +940 -0
  558. package/src/internal-urls/xd-protocol.ts +46 -0
  559. package/src/irc/bus.ts +380 -0
  560. package/src/jsonrpc/message-framing.ts +142 -0
  561. package/src/launch/broker.ts +1368 -0
  562. package/src/launch/client.ts +521 -0
  563. package/src/launch/ensure.ts +73 -0
  564. package/src/launch/paths.ts +14 -0
  565. package/src/launch/presence.ts +82 -0
  566. package/src/launch/protocol.ts +448 -0
  567. package/src/launch/spawn-options.ts +17 -0
  568. package/src/launch/terminal-output-worker-client.ts +53 -0
  569. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  570. package/src/launch/terminal-output-worker.ts +23 -0
  571. package/src/launch/terminal-output.ts +46 -0
  572. package/src/lib/xai-http.ts +150 -0
  573. package/src/live/attestation.ts +91 -0
  574. package/src/live/controller.ts +517 -0
  575. package/src/live/prompts/agent-final-message.md +3 -0
  576. package/src/live/prompts/live-instructions.md +23 -0
  577. package/src/live/protocol.ts +233 -0
  578. package/src/live/transport.ts +422 -0
  579. package/src/live/visualizer.ts +221 -0
  580. package/src/live/voices.ts +18 -0
  581. package/src/lsp/client.ts +1465 -0
  582. package/src/lsp/clients/biome-client.ts +263 -0
  583. package/src/lsp/clients/index.ts +50 -0
  584. package/src/lsp/clients/lsp-linter-client.ts +85 -0
  585. package/src/lsp/clients/swiftlint-client.ts +120 -0
  586. package/src/lsp/config.ts +549 -0
  587. package/src/lsp/defaults.json +499 -0
  588. package/src/lsp/deferred-diagnostics.ts +66 -0
  589. package/src/lsp/diagnostics-ledger.ts +51 -0
  590. package/src/lsp/edits.ts +288 -0
  591. package/src/lsp/format-options.ts +119 -0
  592. package/src/lsp/index.ts +2821 -0
  593. package/src/lsp/lspmux.ts +233 -0
  594. package/src/lsp/mux/daemon.ts +348 -0
  595. package/src/lsp/mux/protocol.ts +96 -0
  596. package/src/lsp/mux/server.ts +797 -0
  597. package/src/lsp/render.ts +668 -0
  598. package/src/lsp/startup-events.ts +13 -0
  599. package/src/lsp/types.ts +479 -0
  600. package/src/lsp/utils.ts +747 -0
  601. package/src/main.ts +1799 -0
  602. package/src/markit/NOTICE +32 -0
  603. package/src/markit/converters/docx.ts +56 -0
  604. package/src/markit/converters/epub.ts +136 -0
  605. package/src/markit/converters/pdf/columns.ts +103 -0
  606. package/src/markit/converters/pdf/extract.ts +598 -0
  607. package/src/markit/converters/pdf/grid.ts +780 -0
  608. package/src/markit/converters/pdf/headers.ts +106 -0
  609. package/src/markit/converters/pdf/index.ts +146 -0
  610. package/src/markit/converters/pdf/render.ts +501 -0
  611. package/src/markit/converters/pdf/types.ts +84 -0
  612. package/src/markit/converters/pptx.ts +325 -0
  613. package/src/markit/converters/xlsx.ts +173 -0
  614. package/src/markit/index.ts +2 -0
  615. package/src/markit/registry.ts +59 -0
  616. package/src/markit/types.ts +35 -0
  617. package/src/mcp/client.ts +511 -0
  618. package/src/mcp/config-writer.ts +377 -0
  619. package/src/mcp/config.ts +385 -0
  620. package/src/mcp/index.ts +29 -0
  621. package/src/mcp/json-rpc.ts +122 -0
  622. package/src/mcp/loader.ts +125 -0
  623. package/src/mcp/manager.ts +1524 -0
  624. package/src/mcp/oauth-credentials.ts +104 -0
  625. package/src/mcp/oauth-discovery.ts +587 -0
  626. package/src/mcp/oauth-flow.ts +830 -0
  627. package/src/mcp/render.ts +214 -0
  628. package/src/mcp/request-id.ts +24 -0
  629. package/src/mcp/smithery-auth.ts +108 -0
  630. package/src/mcp/smithery-connect.ts +154 -0
  631. package/src/mcp/smithery-registry.ts +500 -0
  632. package/src/mcp/startup-events.ts +116 -0
  633. package/src/mcp/timeout.ts +59 -0
  634. package/src/mcp/tool-bridge.ts +691 -0
  635. package/src/mcp/tool-cache.ts +117 -0
  636. package/src/mcp/transports/header-policy.ts +95 -0
  637. package/src/mcp/transports/http.ts +508 -0
  638. package/src/mcp/transports/index.ts +7 -0
  639. package/src/mcp/transports/sse.ts +372 -0
  640. package/src/mcp/transports/stdio.ts +905 -0
  641. package/src/mcp/types.ts +467 -0
  642. package/src/memories/index.ts +1434 -0
  643. package/src/memories/storage.ts +578 -0
  644. package/src/memory-backend/index.ts +19 -0
  645. package/src/memory-backend/local-backend.ts +47 -0
  646. package/src/memory-backend/messages.ts +19 -0
  647. package/src/memory-backend/off-backend.ts +25 -0
  648. package/src/memory-backend/resolve.ts +25 -0
  649. package/src/memory-backend/runtime.ts +66 -0
  650. package/src/memory-backend/tool-names.ts +2 -0
  651. package/src/memory-backend/types.ts +166 -0
  652. package/src/mnemopi/backend.ts +629 -0
  653. package/src/mnemopi/config.ts +267 -0
  654. package/src/mnemopi/embed-client.ts +293 -0
  655. package/src/mnemopi/embed-protocol.ts +35 -0
  656. package/src/mnemopi/embed-worker.ts +114 -0
  657. package/src/mnemopi/index.ts +3 -0
  658. package/src/mnemopi/state.ts +940 -0
  659. package/src/modes/acp/acp-agent.ts +2595 -0
  660. package/src/modes/acp/acp-client-bridge.ts +154 -0
  661. package/src/modes/acp/acp-event-mapper.ts +1084 -0
  662. package/src/modes/acp/acp-mode.ts +48 -0
  663. package/src/modes/acp/index.ts +2 -0
  664. package/src/modes/acp/terminal-auth.ts +37 -0
  665. package/src/modes/components/advisor-config.ts +635 -0
  666. package/src/modes/components/advisor-message.ts +109 -0
  667. package/src/modes/components/agent-dashboard.ts +1252 -0
  668. package/src/modes/components/agent-hub-projection.ts +248 -0
  669. package/src/modes/components/agent-hub-renderer.ts +194 -0
  670. package/src/modes/components/agent-hub.ts +1126 -0
  671. package/src/modes/components/agent-transcript-viewer.ts +647 -0
  672. package/src/modes/components/ask-dialog.ts +1017 -0
  673. package/src/modes/components/assistant-message.ts +978 -0
  674. package/src/modes/components/background-tan-message.ts +36 -0
  675. package/src/modes/components/bash-execution.ts +233 -0
  676. package/src/modes/components/bordered-loader.ts +41 -0
  677. package/src/modes/components/btw-panel.ts +161 -0
  678. package/src/modes/components/cache-invalidation-marker.ts +110 -0
  679. package/src/modes/components/chat-block.ts +111 -0
  680. package/src/modes/components/chat-transcript-builder.ts +518 -0
  681. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  682. package/src/modes/components/collab-prompt-message.ts +32 -0
  683. package/src/modes/components/compaction-summary-message.ts +221 -0
  684. package/src/modes/components/copy-selector.ts +218 -0
  685. package/src/modes/components/countdown-timer.ts +75 -0
  686. package/src/modes/components/custom-editor.ts +1036 -0
  687. package/src/modes/components/custom-message.ts +70 -0
  688. package/src/modes/components/diff.ts +254 -0
  689. package/src/modes/components/dynamic-border.ts +37 -0
  690. package/src/modes/components/error-banner.ts +33 -0
  691. package/src/modes/components/eval-execution.ts +169 -0
  692. package/src/modes/components/execution-shared.ts +101 -0
  693. package/src/modes/components/extensions/extension-dashboard.ts +492 -0
  694. package/src/modes/components/extensions/extension-list.ts +507 -0
  695. package/src/modes/components/extensions/index.ts +9 -0
  696. package/src/modes/components/extensions/inspector-panel.ts +326 -0
  697. package/src/modes/components/extensions/state-manager.ts +648 -0
  698. package/src/modes/components/extensions/types.ts +186 -0
  699. package/src/modes/components/footer.ts +274 -0
  700. package/src/modes/components/history-search.ts +268 -0
  701. package/src/modes/components/hook-editor.ts +213 -0
  702. package/src/modes/components/hook-input.ts +87 -0
  703. package/src/modes/components/hook-message.ts +67 -0
  704. package/src/modes/components/hook-selector.ts +691 -0
  705. package/src/modes/components/index.ts +42 -0
  706. package/src/modes/components/keybinding-hints.ts +56 -0
  707. package/src/modes/components/late-diagnostics-message.ts +60 -0
  708. package/src/modes/components/login-dialog.ts +197 -0
  709. package/src/modes/components/logout-account-selector.ts +130 -0
  710. package/src/modes/components/mcp-add-wizard.ts +1413 -0
  711. package/src/modes/components/message-frame.ts +98 -0
  712. package/src/modes/components/model-browser.ts +888 -0
  713. package/src/modes/components/model-hub.ts +2014 -0
  714. package/src/modes/components/model-picker.ts +237 -0
  715. package/src/modes/components/move-overlay.ts +293 -0
  716. package/src/modes/components/oauth-selector.ts +474 -0
  717. package/src/modes/components/omfg-panel.ts +141 -0
  718. package/src/modes/components/overlay-box.ts +109 -0
  719. package/src/modes/components/pause-screen.ts +208 -0
  720. package/src/modes/components/plan-review-overlay.ts +1226 -0
  721. package/src/modes/components/plan-toc.ts +138 -0
  722. package/src/modes/components/plugin-selector.ts +100 -0
  723. package/src/modes/components/plugin-settings.ts +745 -0
  724. package/src/modes/components/queue-mode-selector.ts +61 -0
  725. package/src/modes/components/read-tool-group.ts +856 -0
  726. package/src/modes/components/reset-usage-selector.ts +161 -0
  727. package/src/modes/components/segment-track.ts +89 -0
  728. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  729. package/src/modes/components/selector-helpers.ts +129 -0
  730. package/src/modes/components/session-account-selector.ts +62 -0
  731. package/src/modes/components/session-selector.ts +1035 -0
  732. package/src/modes/components/settings-defs.ts +267 -0
  733. package/src/modes/components/settings-selector.ts +1445 -0
  734. package/src/modes/components/show-images-selector.ts +50 -0
  735. package/src/modes/components/skill-message.ts +110 -0
  736. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -0
  737. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  738. package/src/modes/components/status-line/component.ts +1843 -0
  739. package/src/modes/components/status-line/context-thresholds.ts +86 -0
  740. package/src/modes/components/status-line/git-utils.ts +42 -0
  741. package/src/modes/components/status-line/index.ts +5 -0
  742. package/src/modes/components/status-line/presets.ts +106 -0
  743. package/src/modes/components/status-line/segments.ts +710 -0
  744. package/src/modes/components/status-line/separators.ts +55 -0
  745. package/src/modes/components/status-line/types.ts +159 -0
  746. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  747. package/src/modes/components/theme-selector.ts +68 -0
  748. package/src/modes/components/thinking-selector.ts +57 -0
  749. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  750. package/src/modes/components/tips.txt +26 -0
  751. package/src/modes/components/todo-reminder.ts +43 -0
  752. package/src/modes/components/tool-execution.ts +1467 -0
  753. package/src/modes/components/transcript-container.ts +527 -0
  754. package/src/modes/components/tree-selector.ts +1006 -0
  755. package/src/modes/components/ttsr-notification.ts +123 -0
  756. package/src/modes/components/usage-row.ts +52 -0
  757. package/src/modes/components/user-message-selector.ts +227 -0
  758. package/src/modes/components/user-message.ts +167 -0
  759. package/src/modes/components/visual-truncate.ts +63 -0
  760. package/src/modes/components/welcome.ts +578 -0
  761. package/src/modes/controllers/btw-controller.ts +246 -0
  762. package/src/modes/controllers/command-controller-shared.ts +109 -0
  763. package/src/modes/controllers/command-controller.ts +2024 -0
  764. package/src/modes/controllers/event-controller.ts +2173 -0
  765. package/src/modes/controllers/extension-ui-controller.ts +1243 -0
  766. package/src/modes/controllers/input-controller.ts +2037 -0
  767. package/src/modes/controllers/live-command-controller.ts +261 -0
  768. package/src/modes/controllers/mcp-command-controller.ts +2552 -0
  769. package/src/modes/controllers/omfg-controller.ts +287 -0
  770. package/src/modes/controllers/omfg-rule.ts +647 -0
  771. package/src/modes/controllers/selector-controller.ts +2058 -0
  772. package/src/modes/controllers/session-focus-controller.ts +117 -0
  773. package/src/modes/controllers/ssh-command-controller.ts +385 -0
  774. package/src/modes/controllers/streaming-reveal.ts +399 -0
  775. package/src/modes/controllers/tan-command-controller.ts +243 -0
  776. package/src/modes/controllers/todo-command-controller.ts +487 -0
  777. package/src/modes/controllers/tool-args-reveal.ts +591 -0
  778. package/src/modes/data/emojis.json +1 -0
  779. package/src/modes/emoji-autocomplete.ts +285 -0
  780. package/src/modes/github-ref-autocomplete.ts +75 -0
  781. package/src/modes/gradient-highlight.ts +99 -0
  782. package/src/modes/image-references.ts +137 -0
  783. package/src/modes/index.ts +10 -0
  784. package/src/modes/interactive-mode.ts +5055 -0
  785. package/src/modes/internal-url-autocomplete.ts +158 -0
  786. package/src/modes/loop-limit.ts +192 -0
  787. package/src/modes/magic-keyword-boundary.ts +23 -0
  788. package/src/modes/magic-keywords.ts +42 -0
  789. package/src/modes/markdown-prose.ts +247 -0
  790. package/src/modes/oauth-manual-input.ts +69 -0
  791. package/src/modes/orchestrate.ts +43 -0
  792. package/src/modes/print-mode.ts +282 -0
  793. package/src/modes/prompt-action-autocomplete.ts +322 -0
  794. package/src/modes/queue-input.ts +132 -0
  795. package/src/modes/rpc/host-tools.ts +204 -0
  796. package/src/modes/rpc/host-uris.ts +241 -0
  797. package/src/modes/rpc/rpc-client.ts +1213 -0
  798. package/src/modes/rpc/rpc-frame.ts +316 -0
  799. package/src/modes/rpc/rpc-input.ts +38 -0
  800. package/src/modes/rpc/rpc-messages.ts +127 -0
  801. package/src/modes/rpc/rpc-mode.ts +1519 -0
  802. package/src/modes/rpc/rpc-subagents.ts +265 -0
  803. package/src/modes/rpc/rpc-types.ts +544 -0
  804. package/src/modes/running-subagent-badge.ts +13 -0
  805. package/src/modes/runtime-init.ts +144 -0
  806. package/src/modes/session-observer-registry.ts +223 -0
  807. package/src/modes/session-teardown.ts +82 -0
  808. package/src/modes/setup-version.ts +11 -0
  809. package/src/modes/setup-wizard/index.ts +103 -0
  810. package/src/modes/setup-wizard/lazy.ts +16 -0
  811. package/src/modes/setup-wizard/scenes/glyph.ts +103 -0
  812. package/src/modes/setup-wizard/scenes/model.ts +132 -0
  813. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  814. package/src/modes/setup-wizard/scenes/providers.ts +105 -0
  815. package/src/modes/setup-wizard/scenes/sign-in.ts +312 -0
  816. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  817. package/src/modes/setup-wizard/scenes/theme.ts +330 -0
  818. package/src/modes/setup-wizard/scenes/types.ts +65 -0
  819. package/src/modes/setup-wizard/scenes/web-search.ts +153 -0
  820. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  821. package/src/modes/setup-wizard/wizard-overlay.ts +335 -0
  822. package/src/modes/shared.ts +49 -0
  823. package/src/modes/skill-command.ts +91 -0
  824. package/src/modes/theme/dark.json +95 -0
  825. package/src/modes/theme/defaults/alabaster.json +93 -0
  826. package/src/modes/theme/defaults/amethyst.json +96 -0
  827. package/src/modes/theme/defaults/anthracite.json +93 -0
  828. package/src/modes/theme/defaults/basalt.json +91 -0
  829. package/src/modes/theme/defaults/birch.json +95 -0
  830. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  831. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  832. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  833. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  834. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  835. package/src/modes/theme/defaults/dark-copper.json +95 -0
  836. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  837. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  838. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  839. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  840. package/src/modes/theme/defaults/dark-ember.json +95 -0
  841. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  842. package/src/modes/theme/defaults/dark-forest.json +96 -0
  843. package/src/modes/theme/defaults/dark-github.json +105 -0
  844. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  845. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  846. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  847. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  848. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  849. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  850. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  851. package/src/modes/theme/defaults/dark-nord.json +97 -0
  852. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  853. package/src/modes/theme/defaults/dark-one.json +100 -0
  854. package/src/modes/theme/defaults/dark-poimandres.json +143 -0
  855. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  856. package/src/modes/theme/defaults/dark-reef.json +91 -0
  857. package/src/modes/theme/defaults/dark-retro.json +92 -0
  858. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  859. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  860. package/src/modes/theme/defaults/dark-slate.json +95 -0
  861. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  862. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  863. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  864. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  865. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  866. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  867. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  868. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  869. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  870. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  871. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  872. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  873. package/src/modes/theme/defaults/graphite.json +92 -0
  874. package/src/modes/theme/defaults/index.ts +199 -0
  875. package/src/modes/theme/defaults/light-arctic.json +107 -0
  876. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  877. package/src/modes/theme/defaults/light-canyon.json +91 -0
  878. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  879. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  880. package/src/modes/theme/defaults/light-coral.json +95 -0
  881. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  882. package/src/modes/theme/defaults/light-dawn.json +90 -0
  883. package/src/modes/theme/defaults/light-dunes.json +91 -0
  884. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  885. package/src/modes/theme/defaults/light-forest.json +100 -0
  886. package/src/modes/theme/defaults/light-frost.json +95 -0
  887. package/src/modes/theme/defaults/light-github.json +115 -0
  888. package/src/modes/theme/defaults/light-glacier.json +91 -0
  889. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  890. package/src/modes/theme/defaults/light-haze.json +90 -0
  891. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  892. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  893. package/src/modes/theme/defaults/light-lavender.json +95 -0
  894. package/src/modes/theme/defaults/light-meadow.json +91 -0
  895. package/src/modes/theme/defaults/light-mint.json +95 -0
  896. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  897. package/src/modes/theme/defaults/light-ocean.json +99 -0
  898. package/src/modes/theme/defaults/light-one.json +99 -0
  899. package/src/modes/theme/defaults/light-opal.json +91 -0
  900. package/src/modes/theme/defaults/light-orchard.json +91 -0
  901. package/src/modes/theme/defaults/light-paper.json +95 -0
  902. package/src/modes/theme/defaults/light-poimandres.json +143 -0
  903. package/src/modes/theme/defaults/light-prism.json +90 -0
  904. package/src/modes/theme/defaults/light-retro.json +98 -0
  905. package/src/modes/theme/defaults/light-sand.json +95 -0
  906. package/src/modes/theme/defaults/light-savanna.json +91 -0
  907. package/src/modes/theme/defaults/light-solarized.json +102 -0
  908. package/src/modes/theme/defaults/light-soleil.json +90 -0
  909. package/src/modes/theme/defaults/light-sunset.json +99 -0
  910. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  911. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  912. package/src/modes/theme/defaults/light-wetland.json +91 -0
  913. package/src/modes/theme/defaults/light-zenith.json +89 -0
  914. package/src/modes/theme/defaults/limestone.json +94 -0
  915. package/src/modes/theme/defaults/mahogany.json +97 -0
  916. package/src/modes/theme/defaults/marble.json +93 -0
  917. package/src/modes/theme/defaults/obsidian.json +91 -0
  918. package/src/modes/theme/defaults/onyx.json +91 -0
  919. package/src/modes/theme/defaults/pearl.json +93 -0
  920. package/src/modes/theme/defaults/porcelain.json +91 -0
  921. package/src/modes/theme/defaults/quartz.json +96 -0
  922. package/src/modes/theme/defaults/sandstone.json +95 -0
  923. package/src/modes/theme/defaults/titanium.json +90 -0
  924. package/src/modes/theme/light.json +93 -0
  925. package/src/modes/theme/mermaid-cache.ts +92 -0
  926. package/src/modes/theme/shimmer.ts +305 -0
  927. package/src/modes/theme/theme-schema.json +463 -0
  928. package/src/modes/theme/theme.ts +3171 -0
  929. package/src/modes/turn-budget.ts +31 -0
  930. package/src/modes/types.ts +494 -0
  931. package/src/modes/ultrathink.ts +42 -0
  932. package/src/modes/utils/context-usage.ts +518 -0
  933. package/src/modes/utils/copy-targets.ts +378 -0
  934. package/src/modes/utils/hotkeys-markdown.ts +65 -0
  935. package/src/modes/utils/interactive-context-helpers.ts +31 -0
  936. package/src/modes/utils/keybinding-matchers.ts +86 -0
  937. package/src/modes/utils/tools-markdown.ts +31 -0
  938. package/src/modes/utils/transcript-render-helpers.ts +253 -0
  939. package/src/modes/utils/ui-helpers.ts +976 -0
  940. package/src/modes/warp-events.ts +232 -0
  941. package/src/modes/workflow.ts +55 -0
  942. package/src/plan-mode/approved-plan.ts +194 -0
  943. package/src/plan-mode/model-transition.ts +51 -0
  944. package/src/plan-mode/plan-files.ts +40 -0
  945. package/src/plan-mode/plan-handoff.ts +37 -0
  946. package/src/plan-mode/plan-protection.ts +31 -0
  947. package/src/plan-mode/state.ts +6 -0
  948. package/src/priority.json +60 -0
  949. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  950. package/src/prompts/advisor/advise-tool.md +3 -0
  951. package/src/prompts/advisor/context-files.md +8 -0
  952. package/src/prompts/advisor/system.md +98 -0
  953. package/src/prompts/agents/designer.md +74 -0
  954. package/src/prompts/agents/frontmatter.md +12 -0
  955. package/src/prompts/agents/init.md +33 -0
  956. package/src/prompts/agents/librarian.md +119 -0
  957. package/src/prompts/agents/reviewer.md +139 -0
  958. package/src/prompts/agents/scout.md +58 -0
  959. package/src/prompts/agents/security-reviewer.md +75 -0
  960. package/src/prompts/agents/task.md +17 -0
  961. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  962. package/src/prompts/bench/cache-prefix.md +3 -0
  963. package/src/prompts/bench/cache-suffix.md +1 -0
  964. package/src/prompts/bench.md +6 -0
  965. package/src/prompts/ci-green-request.md +36 -0
  966. package/src/prompts/dry-balance-bench.md +8 -0
  967. package/src/prompts/goals/goal-budget-limit.md +16 -0
  968. package/src/prompts/goals/goal-continuation.md +28 -0
  969. package/src/prompts/goals/goal-mode-active.md +23 -0
  970. package/src/prompts/goals/goal-mode-context.md +4 -0
  971. package/src/prompts/goals/goal-todo-context.md +12 -0
  972. package/src/prompts/goals/guided-goal-interview.md +43 -0
  973. package/src/prompts/memories/consolidation.md +30 -0
  974. package/src/prompts/memories/consolidation_system.md +4 -0
  975. package/src/prompts/memories/read-path.md +17 -0
  976. package/src/prompts/memories/stage_one_input.md +6 -0
  977. package/src/prompts/memories/stage_one_system.md +21 -0
  978. package/src/prompts/review-custom-request.md +21 -0
  979. package/src/prompts/review-headless-request.md +16 -0
  980. package/src/prompts/review-request.md +68 -0
  981. package/src/prompts/security/scan-coordinator.md +7 -0
  982. package/src/prompts/security/scan-request.md +21 -0
  983. package/src/prompts/security/validate-request.md +8 -0
  984. package/src/prompts/session/launch-completion.md +1 -0
  985. package/src/prompts/skills/autoload.md +8 -0
  986. package/src/prompts/skills/user-invocation.md +11 -0
  987. package/src/prompts/steering/parent-irc.md +5 -0
  988. package/src/prompts/steering/user-interjection.md +6 -0
  989. package/src/prompts/system/active-repo-context.md +4 -0
  990. package/src/prompts/system/agent-creation-architect.md +50 -0
  991. package/src/prompts/system/agent-creation-user.md +6 -0
  992. package/src/prompts/system/auto-continue.md +1 -0
  993. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  994. package/src/prompts/system/auto-thinking-difficulty.md +14 -0
  995. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  996. package/src/prompts/system/autolearn-guidance.md +7 -0
  997. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  998. package/src/prompts/system/background-tan-dispatch.md +8 -0
  999. package/src/prompts/system/btw-user.md +8 -0
  1000. package/src/prompts/system/commit-message-system.md +14 -0
  1001. package/src/prompts/system/computer-safety.md +14 -0
  1002. package/src/prompts/system/custom-system-prompt.md +64 -0
  1003. package/src/prompts/system/eager-task.md +7 -0
  1004. package/src/prompts/system/eager-todo.md +18 -0
  1005. package/src/prompts/system/empty-stop-retry.md +4 -0
  1006. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  1007. package/src/prompts/system/interrupted-thinking.md +7 -0
  1008. package/src/prompts/system/irc-autoreply.md +6 -0
  1009. package/src/prompts/system/irc-incoming.md +9 -0
  1010. package/src/prompts/system/manual-continue.md +7 -0
  1011. package/src/prompts/system/mcp-xdev-guidance.md +11 -0
  1012. package/src/prompts/system/memory-consolidation-system.md +8 -0
  1013. package/src/prompts/system/memory-extraction-system.md +26 -0
  1014. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  1015. package/src/prompts/system/omfg-user.md +50 -0
  1016. package/src/prompts/system/orchestrate-notice.md +40 -0
  1017. package/src/prompts/system/personalities/default.md +18 -0
  1018. package/src/prompts/system/personalities/friendly.md +17 -0
  1019. package/src/prompts/system/personalities/pragmatic.md +15 -0
  1020. package/src/prompts/system/plan-mode-active.md +125 -0
  1021. package/src/prompts/system/plan-mode-approved.md +22 -0
  1022. package/src/prompts/system/plan-mode-compact-instructions.md +17 -0
  1023. package/src/prompts/system/plan-mode-reference.md +10 -0
  1024. package/src/prompts/system/plan-mode-subagent.md +33 -0
  1025. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  1026. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  1027. package/src/prompts/system/prewalk-checklist.md +7 -0
  1028. package/src/prompts/system/prewalk-continue.md +1 -0
  1029. package/src/prompts/system/prewalk-plan.md +13 -0
  1030. package/src/prompts/system/project-prompt.md +61 -0
  1031. package/src/prompts/system/recap-user.md +9 -0
  1032. package/src/prompts/system/resolve-device-reminder.md +3 -0
  1033. package/src/prompts/system/rewind-report.md +6 -0
  1034. package/src/prompts/system/side-channel-no-tools.md +3 -0
  1035. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  1036. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  1037. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  1038. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  1039. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  1040. package/src/prompts/system/speech-rewrite.md +15 -0
  1041. package/src/prompts/system/subagent-async-pending.md +6 -0
  1042. package/src/prompts/system/subagent-system-prompt.md +73 -0
  1043. package/src/prompts/system/subagent-user-prompt.md +3 -0
  1044. package/src/prompts/system/subagent-yield-reminder.md +23 -0
  1045. package/src/prompts/system/system-prompt.md +263 -0
  1046. package/src/prompts/system/tan-context-switch.md +17 -0
  1047. package/src/prompts/system/task-label.md +23 -0
  1048. package/src/prompts/system/thinking-loop-redirect.md +10 -0
  1049. package/src/prompts/system/title-marker-instruction.md +1 -0
  1050. package/src/prompts/system/title-system.md +16 -0
  1051. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  1052. package/src/prompts/system/ttsr-interrupt.md +7 -0
  1053. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  1054. package/src/prompts/system/ultrathink-notice.md +3 -0
  1055. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  1056. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  1057. package/src/prompts/system/vibe-mode-active.md +26 -0
  1058. package/src/prompts/system/web-search.md +25 -0
  1059. package/src/prompts/system/workflow-notice.md +112 -0
  1060. package/src/prompts/system/xdev-mount-notice.md +20 -0
  1061. package/src/prompts/tools/apply-patch.md +65 -0
  1062. package/src/prompts/tools/ask.md +22 -0
  1063. package/src/prompts/tools/ast-edit.md +11 -0
  1064. package/src/prompts/tools/ast-grep.md +19 -0
  1065. package/src/prompts/tools/async-result.md +8 -0
  1066. package/src/prompts/tools/bash.md +23 -0
  1067. package/src/prompts/tools/browser.md +29 -0
  1068. package/src/prompts/tools/checkpoint.md +15 -0
  1069. package/src/prompts/tools/computer.md +26 -0
  1070. package/src/prompts/tools/debug.md +3 -0
  1071. package/src/prompts/tools/eval.md +45 -0
  1072. package/src/prompts/tools/github.md +22 -0
  1073. package/src/prompts/tools/glob.md +16 -0
  1074. package/src/prompts/tools/goal.md +11 -0
  1075. package/src/prompts/tools/grep.md +13 -0
  1076. package/src/prompts/tools/hub.md +34 -0
  1077. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  1078. package/src/prompts/tools/image-attachment-describe.md +10 -0
  1079. package/src/prompts/tools/image-gen.md +7 -0
  1080. package/src/prompts/tools/inspect-image-system.md +20 -0
  1081. package/src/prompts/tools/inspect-image.md +22 -0
  1082. package/src/prompts/tools/learn.md +7 -0
  1083. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  1084. package/src/prompts/tools/lsp.md +19 -0
  1085. package/src/prompts/tools/manage-skill.md +9 -0
  1086. package/src/prompts/tools/memory-edit.md +12 -0
  1087. package/src/prompts/tools/patch.md +57 -0
  1088. package/src/prompts/tools/read.md +27 -0
  1089. package/src/prompts/tools/recall.md +7 -0
  1090. package/src/prompts/tools/reflect.md +5 -0
  1091. package/src/prompts/tools/replace.md +30 -0
  1092. package/src/prompts/tools/retain.md +6 -0
  1093. package/src/prompts/tools/rewind.md +14 -0
  1094. package/src/prompts/tools/security-publish.md +1 -0
  1095. package/src/prompts/tools/security-scan.md +1 -0
  1096. package/src/prompts/tools/task-async-contract.md +1 -0
  1097. package/src/prompts/tools/task-summary.md +20 -0
  1098. package/src/prompts/tools/task.md +83 -0
  1099. package/src/prompts/tools/todo.md +42 -0
  1100. package/src/prompts/tools/vibe-kill.md +3 -0
  1101. package/src/prompts/tools/vibe-list.md +3 -0
  1102. package/src/prompts/tools/vibe-send.md +9 -0
  1103. package/src/prompts/tools/vibe-spawn.md +10 -0
  1104. package/src/prompts/tools/vibe-turn-result.md +19 -0
  1105. package/src/prompts/tools/vibe-wait.md +8 -0
  1106. package/src/prompts/tools/web-search.md +8 -0
  1107. package/src/prompts/tools/write.md +14 -0
  1108. package/src/registry/agent-lifecycle.ts +505 -0
  1109. package/src/registry/agent-registry.ts +287 -0
  1110. package/src/registry/persisted-agents.ts +423 -0
  1111. package/src/sdk.ts +3820 -0
  1112. package/src/secrets/index.ts +375 -0
  1113. package/src/secrets/obfuscator.ts +2629 -0
  1114. package/src/secrets/regex.ts +21 -0
  1115. package/src/security/auth.ts +98 -0
  1116. package/src/security/cloud.ts +686 -0
  1117. package/src/security/comparison.ts +255 -0
  1118. package/src/security/contracts/ids.ts +111 -0
  1119. package/src/security/contracts/index.ts +4 -0
  1120. package/src/security/contracts/schemas.ts +219 -0
  1121. package/src/security/contracts/types.ts +254 -0
  1122. package/src/security/contracts/validation.ts +69 -0
  1123. package/src/security/coordinator.ts +708 -0
  1124. package/src/security/importers/codex-security.ts +387 -0
  1125. package/src/security/importers/index.ts +2 -0
  1126. package/src/security/importers/sarif.ts +357 -0
  1127. package/src/security/index.ts +13 -0
  1128. package/src/security/preflight.ts +405 -0
  1129. package/src/security/provenance.ts +106 -0
  1130. package/src/security/publication.ts +326 -0
  1131. package/src/security/remediation.ts +93 -0
  1132. package/src/security/resource-output.ts +50 -0
  1133. package/src/security/sarif.ts +78 -0
  1134. package/src/security/store.ts +430 -0
  1135. package/src/session/acp-permission-gate.ts +165 -0
  1136. package/src/session/agent-session-events.ts +67 -0
  1137. package/src/session/agent-session-types.ts +407 -0
  1138. package/src/session/agent-session.ts +9000 -0
  1139. package/src/session/agent-storage.ts +807 -0
  1140. package/src/session/artifacts.ts +154 -0
  1141. package/src/session/async-job-delivery.ts +82 -0
  1142. package/src/session/auth-broker-config.ts +92 -0
  1143. package/src/session/auth-storage.ts +25 -0
  1144. package/src/session/bash-runner.ts +326 -0
  1145. package/src/session/blob-store.ts +295 -0
  1146. package/src/session/checkpoint-entries.ts +81 -0
  1147. package/src/session/claude-session-store.ts +426 -0
  1148. package/src/session/client-bridge.ts +85 -0
  1149. package/src/session/codex-auto-reset.ts +673 -0
  1150. package/src/session/codex-session-store.ts +673 -0
  1151. package/src/session/compact-modes.ts +105 -0
  1152. package/src/session/credential-pin.ts +93 -0
  1153. package/src/session/eval-runner.ts +219 -0
  1154. package/src/session/exit-diagnostics.ts +310 -0
  1155. package/src/session/foreign-session-import.ts +52 -0
  1156. package/src/session/foreign-session-jsonl.ts +29 -0
  1157. package/src/session/foreign-session-store.ts +26 -0
  1158. package/src/session/history-storage.ts +329 -0
  1159. package/src/session/indexed-session-storage.ts +553 -0
  1160. package/src/session/irc-bridge.ts +203 -0
  1161. package/src/session/launch-completion.ts +37 -0
  1162. package/src/session/messages.ts +1304 -0
  1163. package/src/session/model-controls.ts +757 -0
  1164. package/src/session/prewalk.ts +279 -0
  1165. package/src/session/provider-image-budget.ts +86 -0
  1166. package/src/session/queued-messages.ts +99 -0
  1167. package/src/session/redis-session-storage.ts +257 -0
  1168. package/src/session/retry-fallback-chains.ts +455 -0
  1169. package/src/session/role-models.ts +85 -0
  1170. package/src/session/session-advisors.ts +1896 -0
  1171. package/src/session/session-context.ts +584 -0
  1172. package/src/session/session-dump-format.ts +216 -0
  1173. package/src/session/session-entries.ts +306 -0
  1174. package/src/session/session-handoff.ts +325 -0
  1175. package/src/session/session-history-format.ts +459 -0
  1176. package/src/session/session-listing.ts +715 -0
  1177. package/src/session/session-loader.ts +362 -0
  1178. package/src/session/session-maintenance.ts +3115 -0
  1179. package/src/session/session-manager.ts +2747 -0
  1180. package/src/session/session-memory.ts +222 -0
  1181. package/src/session/session-metadata.ts +53 -0
  1182. package/src/session/session-migrations.ts +78 -0
  1183. package/src/session/session-paths.ts +280 -0
  1184. package/src/session/session-persistence.ts +293 -0
  1185. package/src/session/session-provider-boundary.ts +306 -0
  1186. package/src/session/session-stats.ts +349 -0
  1187. package/src/session/session-storage.ts +774 -0
  1188. package/src/session/session-title-slot.ts +141 -0
  1189. package/src/session/session-tools.ts +1385 -0
  1190. package/src/session/session-workspace.ts +53 -0
  1191. package/src/session/settings-stream-fn.ts +79 -0
  1192. package/src/session/shake-types.ts +43 -0
  1193. package/src/session/snapcompact-inline.ts +545 -0
  1194. package/src/session/snapcompact-savings-journal.ts +113 -0
  1195. package/src/session/sql-session-storage.ts +374 -0
  1196. package/src/session/stream-guards.ts +417 -0
  1197. package/src/session/streaming-output.ts +1459 -0
  1198. package/src/session/todo-tracker.ts +380 -0
  1199. package/src/session/tool-choice-queue.ts +305 -0
  1200. package/src/session/ttsr-coordinator.ts +496 -0
  1201. package/src/session/turn-persistence.ts +142 -0
  1202. package/src/session/turn-recovery.ts +2005 -0
  1203. package/src/session/unexpected-stop-classifier.ts +141 -0
  1204. package/src/session/yield-queue.ts +288 -0
  1205. package/src/slash-commands/acp-builtins.ts +70 -0
  1206. package/src/slash-commands/available-commands.ts +105 -0
  1207. package/src/slash-commands/builtin-registry.ts +3149 -0
  1208. package/src/slash-commands/helpers/active-oauth-account.ts +80 -0
  1209. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  1210. package/src/slash-commands/helpers/context-report.ts +66 -0
  1211. package/src/slash-commands/helpers/format.ts +46 -0
  1212. package/src/slash-commands/helpers/logout.ts +108 -0
  1213. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  1214. package/src/slash-commands/helpers/mcp.ts +533 -0
  1215. package/src/slash-commands/helpers/parse.ts +85 -0
  1216. package/src/slash-commands/helpers/reset-usage.ts +68 -0
  1217. package/src/slash-commands/helpers/security.ts +451 -0
  1218. package/src/slash-commands/helpers/session-pin.ts +44 -0
  1219. package/src/slash-commands/helpers/ssh.ts +196 -0
  1220. package/src/slash-commands/helpers/stats-dashboard.ts +86 -0
  1221. package/src/slash-commands/helpers/todo.ts +285 -0
  1222. package/src/slash-commands/helpers/usage-report.ts +198 -0
  1223. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  1224. package/src/slash-commands/types.ts +139 -0
  1225. package/src/ssh/config-writer.ts +183 -0
  1226. package/src/ssh/connection-manager.ts +667 -0
  1227. package/src/ssh/file-transfer.ts +214 -0
  1228. package/src/ssh/sshfs-mount.ts +163 -0
  1229. package/src/ssh/utils.ts +51 -0
  1230. package/src/startup-splash.ts +19 -0
  1231. package/src/stt/asr-client.ts +401 -0
  1232. package/src/stt/asr-protocol.ts +65 -0
  1233. package/src/stt/asr-worker.ts +603 -0
  1234. package/src/stt/downloader.ts +142 -0
  1235. package/src/stt/endpointer.ts +259 -0
  1236. package/src/stt/index.ts +6 -0
  1237. package/src/stt/models.ts +150 -0
  1238. package/src/stt/sherpa-runtime.ts +71 -0
  1239. package/src/stt/stt-controller.ts +320 -0
  1240. package/src/stt/submit-trigger.ts +74 -0
  1241. package/src/subprocess/worker-client.ts +463 -0
  1242. package/src/subprocess/worker-runtime.ts +494 -0
  1243. package/src/system-prompt.ts +921 -0
  1244. package/src/task/agents.ts +170 -0
  1245. package/src/task/commands.ts +132 -0
  1246. package/src/task/discovery.ts +145 -0
  1247. package/src/task/executor.ts +3431 -0
  1248. package/src/task/index.ts +1515 -0
  1249. package/src/task/isolation-ownership.ts +106 -0
  1250. package/src/task/isolation-runner.ts +444 -0
  1251. package/src/task/label.ts +40 -0
  1252. package/src/task/name-generator.ts +1577 -0
  1253. package/src/task/omp-command.ts +26 -0
  1254. package/src/task/output-manager.ts +115 -0
  1255. package/src/task/parallel.ts +221 -0
  1256. package/src/task/persisted-revive.ts +173 -0
  1257. package/src/task/prewalk.ts +6 -0
  1258. package/src/task/prompt-policy.ts +8 -0
  1259. package/src/task/provider-concurrency.ts +100 -0
  1260. package/src/task/read-only-policy.ts +27 -0
  1261. package/src/task/render.ts +1830 -0
  1262. package/src/task/renderer.ts +14 -0
  1263. package/src/task/repair-args.ts +118 -0
  1264. package/src/task/spawn-policy.ts +72 -0
  1265. package/src/task/structured-subagent.ts +676 -0
  1266. package/src/task/subprocess-tool-registry.ts +88 -0
  1267. package/src/task/types.ts +555 -0
  1268. package/src/task/worktree.ts +967 -0
  1269. package/src/task/yield-assembly.ts +198 -0
  1270. package/src/telemetry-export.ts +504 -0
  1271. package/src/thinking.ts +377 -0
  1272. package/src/tiny/device.ts +111 -0
  1273. package/src/tiny/dtype.ts +101 -0
  1274. package/src/tiny/message-preproc.ts +155 -0
  1275. package/src/tiny/models.ts +268 -0
  1276. package/src/tiny/text.ts +290 -0
  1277. package/src/tiny/title-client.ts +460 -0
  1278. package/src/tiny/title-protocol.ts +56 -0
  1279. package/src/tiny/worker.ts +354 -0
  1280. package/src/tools/acp-bridge.ts +125 -0
  1281. package/src/tools/approval.ts +245 -0
  1282. package/src/tools/ask.ts +1459 -0
  1283. package/src/tools/ast-edit.ts +718 -0
  1284. package/src/tools/ast-grep.ts +526 -0
  1285. package/src/tools/auto-generated-guard.ts +335 -0
  1286. package/src/tools/bash-interactive.ts +435 -0
  1287. package/src/tools/bash-interceptor.ts +148 -0
  1288. package/src/tools/bash-pty-selection.ts +14 -0
  1289. package/src/tools/bash-skill-urls.ts +350 -0
  1290. package/src/tools/bash.ts +1826 -0
  1291. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  1292. package/src/tools/browser/aria/aria-snapshot.ts +131 -0
  1293. package/src/tools/browser/attach.ts +219 -0
  1294. package/src/tools/browser/cmux/cmux-tab.ts +1531 -0
  1295. package/src/tools/browser/cmux/rpc.ts +200 -0
  1296. package/src/tools/browser/cmux/socket-client.ts +445 -0
  1297. package/src/tools/browser/launch.ts +939 -0
  1298. package/src/tools/browser/readable.ts +112 -0
  1299. package/src/tools/browser/registry.ts +447 -0
  1300. package/src/tools/browser/relay/bridge.ts +945 -0
  1301. package/src/tools/browser/relay/daemon.ts +117 -0
  1302. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  1303. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  1304. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  1305. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  1306. package/src/tools/browser/relay/kind.ts +41 -0
  1307. package/src/tools/browser/relay/protocol.ts +54 -0
  1308. package/src/tools/browser/relay/server.ts +141 -0
  1309. package/src/tools/browser/render.ts +229 -0
  1310. package/src/tools/browser/run-output.ts +76 -0
  1311. package/src/tools/browser/shared-daemon.ts +139 -0
  1312. package/src/tools/browser/tab-protocol.ts +123 -0
  1313. package/src/tools/browser/tab-supervisor.ts +1077 -0
  1314. package/src/tools/browser/tab-worker-entry.ts +29 -0
  1315. package/src/tools/browser/tab-worker.ts +1982 -0
  1316. package/src/tools/browser.ts +487 -0
  1317. package/src/tools/builtin-names.ts +67 -0
  1318. package/src/tools/checkpoint.ts +137 -0
  1319. package/src/tools/computer/exposure.ts +14 -0
  1320. package/src/tools/computer/protocol.ts +69 -0
  1321. package/src/tools/computer/supervisor.ts +409 -0
  1322. package/src/tools/computer/worker-entry.ts +39 -0
  1323. package/src/tools/computer/worker.ts +745 -0
  1324. package/src/tools/computer-renderer.ts +147 -0
  1325. package/src/tools/computer.ts +222 -0
  1326. package/src/tools/conflict-detect.ts +815 -0
  1327. package/src/tools/context.ts +49 -0
  1328. package/src/tools/debug.ts +1121 -0
  1329. package/src/tools/default-renderer.ts +154 -0
  1330. package/src/tools/essential-tools.ts +46 -0
  1331. package/src/tools/eval-backends.ts +34 -0
  1332. package/src/tools/eval-format/index.ts +24 -0
  1333. package/src/tools/eval-format/javascript.ts +952 -0
  1334. package/src/tools/eval-format/julia.ts +446 -0
  1335. package/src/tools/eval-format/python.ts +544 -0
  1336. package/src/tools/eval-format/ruby.ts +380 -0
  1337. package/src/tools/eval-render.ts +784 -0
  1338. package/src/tools/eval.ts +774 -0
  1339. package/src/tools/fetch.ts +1889 -0
  1340. package/src/tools/file-recorder.ts +35 -0
  1341. package/src/tools/fs-cache-invalidation.ts +28 -0
  1342. package/src/tools/gh-cache-invalidation.ts +175 -0
  1343. package/src/tools/gh-format.ts +12 -0
  1344. package/src/tools/gh-renderer.ts +484 -0
  1345. package/src/tools/gh.ts +3958 -0
  1346. package/src/tools/github-cache.ts +663 -0
  1347. package/src/tools/glob.ts +691 -0
  1348. package/src/tools/grep.ts +1920 -0
  1349. package/src/tools/grouped-file-output.ts +210 -0
  1350. package/src/tools/hub/index.ts +579 -0
  1351. package/src/tools/hub/jobs.ts +714 -0
  1352. package/src/tools/hub/launch.ts +690 -0
  1353. package/src/tools/hub/messaging.ts +735 -0
  1354. package/src/tools/hub/types.ts +117 -0
  1355. package/src/tools/image-gen.ts +1689 -0
  1356. package/src/tools/image-providers.ts +50 -0
  1357. package/src/tools/index.ts +736 -0
  1358. package/src/tools/inspect-image-renderer.ts +133 -0
  1359. package/src/tools/inspect-image.ts +316 -0
  1360. package/src/tools/json-tree.ts +260 -0
  1361. package/src/tools/jtd-to-json-schema.ts +219 -0
  1362. package/src/tools/jtd-to-typescript.ts +136 -0
  1363. package/src/tools/jtd-utils.ts +102 -0
  1364. package/src/tools/learn.ts +141 -0
  1365. package/src/tools/list-limit.ts +40 -0
  1366. package/src/tools/manage-skill.ts +102 -0
  1367. package/src/tools/match-line-format.ts +20 -0
  1368. package/src/tools/memory-edit.ts +61 -0
  1369. package/src/tools/memory-recall.ts +102 -0
  1370. package/src/tools/memory-reflect.ts +88 -0
  1371. package/src/tools/memory-render.ts +211 -0
  1372. package/src/tools/memory-retain.ts +89 -0
  1373. package/src/tools/output-meta.ts +860 -0
  1374. package/src/tools/output-schema-validator.ts +307 -0
  1375. package/src/tools/path-utils.ts +1489 -0
  1376. package/src/tools/plan-mode-guard.ts +155 -0
  1377. package/src/tools/puppeteer/00_stealth_tampering.txt +44 -0
  1378. package/src/tools/puppeteer/01_stealth_activity.txt +80 -0
  1379. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -0
  1380. package/src/tools/puppeteer/03_stealth_botd.txt +380 -0
  1381. package/src/tools/puppeteer/04_stealth_iframe.txt +174 -0
  1382. package/src/tools/puppeteer/05_stealth_webgl.txt +233 -0
  1383. package/src/tools/puppeteer/06_stealth_screen.txt +260 -0
  1384. package/src/tools/puppeteer/07_stealth_fonts.txt +99 -0
  1385. package/src/tools/puppeteer/08_stealth_audio.txt +63 -0
  1386. package/src/tools/puppeteer/09_stealth_locale.txt +51 -0
  1387. package/src/tools/puppeteer/10_stealth_plugins.txt +212 -0
  1388. package/src/tools/puppeteer/11_stealth_hardware.txt +59 -0
  1389. package/src/tools/puppeteer/12_stealth_codecs.txt +42 -0
  1390. package/src/tools/puppeteer/13_stealth_worker.txt +235 -0
  1391. package/src/tools/read.ts +3762 -0
  1392. package/src/tools/render-utils.ts +926 -0
  1393. package/src/tools/renderers.ts +133 -0
  1394. package/src/tools/report-tool-issue.ts +568 -0
  1395. package/src/tools/resolve.ts +423 -0
  1396. package/src/tools/review.ts +103 -0
  1397. package/src/tools/run-scope.ts +417 -0
  1398. package/src/tools/security-scan.ts +287 -0
  1399. package/src/tools/shell-tokenize.ts +213 -0
  1400. package/src/tools/sqlite-reader.ts +884 -0
  1401. package/src/tools/terminal-output.ts +141 -0
  1402. package/src/tools/todo.ts +1226 -0
  1403. package/src/tools/tool-errors.ts +62 -0
  1404. package/src/tools/tool-result.ts +102 -0
  1405. package/src/tools/tool-timeouts.ts +38 -0
  1406. package/src/tools/tts.ts +266 -0
  1407. package/src/tools/vibe.ts +608 -0
  1408. package/src/tools/write.ts +1646 -0
  1409. package/src/tools/xdev.ts +560 -0
  1410. package/src/tools/yield.ts +486 -0
  1411. package/src/tts/downloader.ts +64 -0
  1412. package/src/tts/index.ts +10 -0
  1413. package/src/tts/models.ts +137 -0
  1414. package/src/tts/runtime.ts +21 -0
  1415. package/src/tts/speakable.ts +392 -0
  1416. package/src/tts/speech-enhancer.ts +206 -0
  1417. package/src/tts/streaming-player.ts +120 -0
  1418. package/src/tts/tts-client.ts +475 -0
  1419. package/src/tts/tts-protocol.ts +69 -0
  1420. package/src/tts/tts-worker.ts +434 -0
  1421. package/src/tts/vocalizer.ts +419 -0
  1422. package/src/tts/wav.ts +58 -0
  1423. package/src/tui/code-cell.ts +268 -0
  1424. package/src/tui/file-list.ts +55 -0
  1425. package/src/tui/hyperlink.ts +178 -0
  1426. package/src/tui/index.ts +13 -0
  1427. package/src/tui/output-block.ts +268 -0
  1428. package/src/tui/status-line.ts +54 -0
  1429. package/src/tui/tree-list.ts +172 -0
  1430. package/src/tui/types.ts +15 -0
  1431. package/src/tui/utils.ts +103 -0
  1432. package/src/tui/width-aware-text.ts +58 -0
  1433. package/src/utils/active-repo-context.ts +143 -0
  1434. package/src/utils/block-context.ts +312 -0
  1435. package/src/utils/changelog.ts +373 -0
  1436. package/src/utils/clipboard.ts +361 -0
  1437. package/src/utils/command-args.ts +74 -0
  1438. package/src/utils/commit-message-generator.ts +148 -0
  1439. package/src/utils/cpuprofile.ts +235 -0
  1440. package/src/utils/edit-mode.ts +61 -0
  1441. package/src/utils/enhanced-paste.ts +230 -0
  1442. package/src/utils/event-bus.ts +33 -0
  1443. package/src/utils/external-editor.ts +80 -0
  1444. package/src/utils/fetch-timeout.ts +10 -0
  1445. package/src/utils/file-display-mode.ts +44 -0
  1446. package/src/utils/file-mentions.ts +293 -0
  1447. package/src/utils/git.ts +2472 -0
  1448. package/src/utils/image-loading.ts +236 -0
  1449. package/src/utils/image-resize.ts +420 -0
  1450. package/src/utils/image-vision-fallback.ts +196 -0
  1451. package/src/utils/inspect-image-mode.ts +39 -0
  1452. package/src/utils/ipc.ts +38 -0
  1453. package/src/utils/jj.ts +422 -0
  1454. package/src/utils/lang-from-path.ts +251 -0
  1455. package/src/utils/late-cleanup.ts +17 -0
  1456. package/src/utils/local-date.ts +7 -0
  1457. package/src/utils/mac-file-urls.applescript +37 -0
  1458. package/src/utils/markit-cache.ts +166 -0
  1459. package/src/utils/markit.ts +223 -0
  1460. package/src/utils/mupdf-wasm-embed.ts +12 -0
  1461. package/src/utils/open.ts +126 -0
  1462. package/src/utils/profile-tree.ts +111 -0
  1463. package/src/utils/prompt-path.ts +3 -0
  1464. package/src/utils/qrcode.ts +535 -0
  1465. package/src/utils/sample-profile.ts +437 -0
  1466. package/src/utils/session-color.ts +142 -0
  1467. package/src/utils/shell-snapshot-fn-env.sh +63 -0
  1468. package/src/utils/shell-snapshot.ts +326 -0
  1469. package/src/utils/sixel.ts +69 -0
  1470. package/src/utils/thinking-display.ts +163 -0
  1471. package/src/utils/title-generator.ts +597 -0
  1472. package/src/utils/token-rate.ts +72 -0
  1473. package/src/utils/tool-choice.ts +50 -0
  1474. package/src/utils/tools-manager.ts +411 -0
  1475. package/src/utils/turndown.ts +82 -0
  1476. package/src/utils/zip.ts +1106 -0
  1477. package/src/vibe/runtime.ts +1684 -0
  1478. package/src/vibe/state.ts +4 -0
  1479. package/src/web/kagi.ts +305 -0
  1480. package/src/web/parallel.ts +354 -0
  1481. package/src/web/scrapers/artifacthub.ts +207 -0
  1482. package/src/web/scrapers/arxiv.ts +83 -0
  1483. package/src/web/scrapers/aur.ts +162 -0
  1484. package/src/web/scrapers/biorxiv.ts +133 -0
  1485. package/src/web/scrapers/bluesky.ts +262 -0
  1486. package/src/web/scrapers/brew.ts +172 -0
  1487. package/src/web/scrapers/cheatsh.ts +68 -0
  1488. package/src/web/scrapers/chocolatey.ts +196 -0
  1489. package/src/web/scrapers/choosealicense.ts +95 -0
  1490. package/src/web/scrapers/cisa-kev.ts +87 -0
  1491. package/src/web/scrapers/clojars.ts +154 -0
  1492. package/src/web/scrapers/coingecko.ts +177 -0
  1493. package/src/web/scrapers/crates-io.ts +97 -0
  1494. package/src/web/scrapers/crossref.ts +136 -0
  1495. package/src/web/scrapers/devto.ts +147 -0
  1496. package/src/web/scrapers/discogs.ts +306 -0
  1497. package/src/web/scrapers/discourse.ts +197 -0
  1498. package/src/web/scrapers/dockerhub.ts +138 -0
  1499. package/src/web/scrapers/docs-rs.ts +663 -0
  1500. package/src/web/scrapers/fdroid.ts +134 -0
  1501. package/src/web/scrapers/firefox-addons.ts +191 -0
  1502. package/src/web/scrapers/flathub.ts +223 -0
  1503. package/src/web/scrapers/github-gist.ts +58 -0
  1504. package/src/web/scrapers/github.ts +800 -0
  1505. package/src/web/scrapers/gitlab.ts +401 -0
  1506. package/src/web/scrapers/go-pkg.ts +266 -0
  1507. package/src/web/scrapers/hackage.ts +129 -0
  1508. package/src/web/scrapers/hackernews.ts +189 -0
  1509. package/src/web/scrapers/hex.ts +105 -0
  1510. package/src/web/scrapers/huggingface.ts +321 -0
  1511. package/src/web/scrapers/iacr.ts +89 -0
  1512. package/src/web/scrapers/index.ts +252 -0
  1513. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1514. package/src/web/scrapers/lemmy.ts +203 -0
  1515. package/src/web/scrapers/lobsters.ts +175 -0
  1516. package/src/web/scrapers/mastodon.ts +292 -0
  1517. package/src/web/scrapers/maven.ts +138 -0
  1518. package/src/web/scrapers/mdn.ts +173 -0
  1519. package/src/web/scrapers/metacpan.ts +222 -0
  1520. package/src/web/scrapers/musicbrainz.ts +250 -0
  1521. package/src/web/scrapers/npm.ts +98 -0
  1522. package/src/web/scrapers/nuget.ts +183 -0
  1523. package/src/web/scrapers/nvd.ts +222 -0
  1524. package/src/web/scrapers/ollama.ts +239 -0
  1525. package/src/web/scrapers/open-vsx.ts +106 -0
  1526. package/src/web/scrapers/opencorporates.ts +292 -0
  1527. package/src/web/scrapers/openlibrary.ts +336 -0
  1528. package/src/web/scrapers/orcid.ts +286 -0
  1529. package/src/web/scrapers/osv.ts +176 -0
  1530. package/src/web/scrapers/packagist.ts +160 -0
  1531. package/src/web/scrapers/pub-dev.ts +143 -0
  1532. package/src/web/scrapers/pubmed.ts +211 -0
  1533. package/src/web/scrapers/pypi.ts +112 -0
  1534. package/src/web/scrapers/rawg.ts +110 -0
  1535. package/src/web/scrapers/readthedocs.ts +121 -0
  1536. package/src/web/scrapers/reddit.ts +95 -0
  1537. package/src/web/scrapers/repology.ts +251 -0
  1538. package/src/web/scrapers/rfc.ts +201 -0
  1539. package/src/web/scrapers/rubygems.ts +103 -0
  1540. package/src/web/scrapers/searchcode.ts +189 -0
  1541. package/src/web/scrapers/sec-edgar.ts +261 -0
  1542. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1543. package/src/web/scrapers/snapcraft.ts +187 -0
  1544. package/src/web/scrapers/sourcegraph.ts +336 -0
  1545. package/src/web/scrapers/spdx.ts +108 -0
  1546. package/src/web/scrapers/spotify.ts +198 -0
  1547. package/src/web/scrapers/stackoverflow.ts +120 -0
  1548. package/src/web/scrapers/terraform.ts +277 -0
  1549. package/src/web/scrapers/tldr.ts +47 -0
  1550. package/src/web/scrapers/twitter.ts +94 -0
  1551. package/src/web/scrapers/types.ts +354 -0
  1552. package/src/web/scrapers/utils.ts +109 -0
  1553. package/src/web/scrapers/vimeo.ts +133 -0
  1554. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1555. package/src/web/scrapers/w3c.ts +156 -0
  1556. package/src/web/scrapers/wikidata.ts +344 -0
  1557. package/src/web/scrapers/wikipedia.ts +84 -0
  1558. package/src/web/scrapers/youtube.ts +325 -0
  1559. package/src/web/search/index.ts +393 -0
  1560. package/src/web/search/provider.ts +272 -0
  1561. package/src/web/search/providers/anthropic.ts +406 -0
  1562. package/src/web/search/providers/base.ts +112 -0
  1563. package/src/web/search/providers/brave.ts +181 -0
  1564. package/src/web/search/providers/browser-headers.ts +82 -0
  1565. package/src/web/search/providers/browser-page.ts +125 -0
  1566. package/src/web/search/providers/codex.ts +788 -0
  1567. package/src/web/search/providers/duckduckgo.ts +382 -0
  1568. package/src/web/search/providers/ecosia.ts +183 -0
  1569. package/src/web/search/providers/exa.ts +513 -0
  1570. package/src/web/search/providers/firecrawl.ts +213 -0
  1571. package/src/web/search/providers/gemini.ts +634 -0
  1572. package/src/web/search/providers/google.ts +195 -0
  1573. package/src/web/search/providers/jina.ts +144 -0
  1574. package/src/web/search/providers/kagi.ts +98 -0
  1575. package/src/web/search/providers/kimi.ts +221 -0
  1576. package/src/web/search/providers/mojeek.ts +220 -0
  1577. package/src/web/search/providers/parallel.ts +186 -0
  1578. package/src/web/search/providers/perplexity-auth.ts +142 -0
  1579. package/src/web/search/providers/perplexity.ts +997 -0
  1580. package/src/web/search/providers/public.ts +199 -0
  1581. package/src/web/search/providers/searxng.ts +467 -0
  1582. package/src/web/search/providers/startpage.ts +225 -0
  1583. package/src/web/search/providers/synthetic.ts +126 -0
  1584. package/src/web/search/providers/tavily.ts +244 -0
  1585. package/src/web/search/providers/tinyfish.ts +166 -0
  1586. package/src/web/search/providers/utils.ts +130 -0
  1587. package/src/web/search/providers/xai.ts +372 -0
  1588. package/src/web/search/providers/zai.ts +452 -0
  1589. package/src/web/search/query.ts +850 -0
  1590. package/src/web/search/render.ts +262 -0
  1591. package/src/web/search/types.ts +518 -0
  1592. package/src/web/search/utils.ts +17 -0
  1593. package/src/workspace-tree.ts +326 -0
@@ -0,0 +1,3115 @@
1
+ /** Context maintenance for an active coding-agent session. */
2
+
3
+ import { scheduler } from "node:timers/promises";
4
+ import {
5
+ type Agent,
6
+ type AgentMessage,
7
+ type AgentTurnEndContext,
8
+ countTokens,
9
+ resolveTelemetry,
10
+ type StreamFn,
11
+ type ThinkingLevel,
12
+ } from "@oh-my-pi/pi-agent-core";
13
+ import {
14
+ AGGRESSIVE_SHAKE_CONFIG,
15
+ AUTO_HANDOFF_THRESHOLD_FOCUS,
16
+ applyShakeRegions,
17
+ CompactionCancelledError,
18
+ type CompactionPreparation,
19
+ type CompactionResult,
20
+ type CompactionSettings,
21
+ calculateContextTokens,
22
+ collectShakeRegions,
23
+ compact,
24
+ compactionContextTokens,
25
+ createCompactionSummaryMessage,
26
+ DEFAULT_SHAKE_CONFIG,
27
+ effectiveReserveTokens,
28
+ estimateTokens,
29
+ hasContextTokenUsage,
30
+ NativeCompactionError,
31
+ prepareCompaction,
32
+ RESCUE_SHAKE_CONFIG,
33
+ resolveBudgetReserveTokens,
34
+ resolveThresholdTokens,
35
+ type ShakeConfig,
36
+ type ShakeRegion,
37
+ type SummaryOptions,
38
+ shouldCompact,
39
+ shouldUseOpenAiRemoteCompaction,
40
+ shouldUseProviderNativeCompaction,
41
+ } from "@oh-my-pi/pi-agent-core/compaction";
42
+ import {
43
+ DEFAULT_PRUNE_CONFIG,
44
+ pruneSupersededToolResults,
45
+ pruneToolOutputs,
46
+ readToolSupersedeKey,
47
+ } from "@oh-my-pi/pi-agent-core/compaction/pruning";
48
+ import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
49
+ import type { AssistantMessage, CodexCompactionContext, Message, Model, ProviderSessionState } from "@oh-my-pi/pi-ai";
50
+ import * as AIError from "@oh-my-pi/pi-ai/error";
51
+ import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
52
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
53
+ import { logger } from "@oh-my-pi/pi-utils";
54
+ import * as snapcompact from "@oh-my-pi/snapcompact";
55
+ import type { ModelRegistry } from "../config/model-registry";
56
+ import { MODEL_ROLE_IDS } from "../config/model-roles";
57
+ import type { Settings } from "../config/settings";
58
+ import { getDefault } from "../config/settings";
59
+ import type { ExtensionRunner, SessionBeforeCompactResult } from "../extensibility/extensions";
60
+ import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
61
+ import type { GoalModeState } from "../goals/state";
62
+ import { resolveMemoryBackend } from "../memory-backend/resolve";
63
+ import type { MemoryBackendOperationContext } from "../memory-backend/types";
64
+ import type { NonMessageTokenSource } from "../modes/utils/context-usage";
65
+ import { computeNonMessageTokens } from "../modes/utils/context-usage";
66
+ import { createPlanReadMatcher } from "../plan-mode/plan-protection";
67
+ import type { ConfiguredThinkingLevel } from "../thinking";
68
+ import type { AgentSessionEvent } from "./agent-session-events";
69
+ import type { ContextUsageBreakdown, HandoffResult, SessionHandoffOptions } from "./agent-session-types";
70
+ import { findCompactMode } from "./compact-modes";
71
+ import { convertToLlm, stripImagesFromMessage } from "./messages";
72
+ import { isTerminalTextAssistantAnswer } from "./queued-messages";
73
+ import {
74
+ resolveCompactionConfiguredTarget,
75
+ resolveContextPromotionConfiguredTarget,
76
+ resolveRoleModelFull,
77
+ } from "./role-models";
78
+ import type { SessionContext } from "./session-context";
79
+ import { getLatestCompactionEntry, getOpenAiRemoteCompactionPayload } from "./session-context";
80
+ import type { CompactionEntry, SessionEntry } from "./session-entries";
81
+ import type { SessionManager } from "./session-manager";
82
+ import type { ShakeMode, ShakeResult } from "./shake-types";
83
+
84
+ export type CompactionCheckResult = Readonly<{
85
+ deferredHandoff: boolean;
86
+ continuationScheduled: boolean;
87
+ automaticContinuationBlocked?: boolean;
88
+ historyRewritten?: boolean;
89
+ }>;
90
+
91
+ /** Shared no-op result for dispatcher paths that perform no maintenance. */
92
+ export const COMPACTION_CHECK_NONE: CompactionCheckResult = {
93
+ deferredHandoff: false,
94
+ continuationScheduled: false,
95
+ };
96
+ const COMPACTION_CHECK_DEFERRED_HANDOFF: CompactionCheckResult = {
97
+ deferredHandoff: true,
98
+ continuationScheduled: false,
99
+ };
100
+ const COMPACTION_CHECK_CONTINUATION: CompactionCheckResult = {
101
+ deferredHandoff: false,
102
+ continuationScheduled: true,
103
+ };
104
+ const COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION: CompactionCheckResult = {
105
+ deferredHandoff: false,
106
+ continuationScheduled: false,
107
+ automaticContinuationBlocked: true,
108
+ };
109
+
110
+ /**
111
+ * User-facing notice for a compaction dead end: maintenance freed too little
112
+ * to retry safely. `remedies` names the recovery actions left on the emitting
113
+ * path — by the time the post-pass dead end fires, the tiered rescue has
114
+ * already attempted both elide and image-drop automatically.
115
+ */
116
+ function compactionDeadEndWarning(remedies: string): string {
117
+ return (
118
+ "Compaction freed too little context to make progress — pausing automatic maintenance to avoid a compaction loop. " +
119
+ `The most recent turn alone is too large to reduce further; ${remedies} or switch to a larger-context model.`
120
+ );
121
+ }
122
+
123
+ /** Creates one provider-scoped compaction lifecycle descriptor. */
124
+ export function createCodexCompactionContext(options: {
125
+ trigger: CodexCompactionContext["trigger"];
126
+ reason: CodexCompactionContext["reason"];
127
+ phase: CodexCompactionContext["phase"];
128
+ }): CodexCompactionContext {
129
+ return {
130
+ operationId: crypto.randomUUID(),
131
+ trigger: options.trigger,
132
+ reason: options.reason,
133
+ phase: options.phase,
134
+ strategy: "memento",
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Per-turn prune cache window. A tool result whose all-message suffix exceeds
140
+ * this is in the warm, already-sent prompt-cache prefix: re-writing it costs the
141
+ * cacheWrite premium on the whole suffix. Per-turn passes only reclaim inside
142
+ * this tail (matches the supersede pass's default `suffixTokenLimit`); deeper
143
+ * stale/age victims are left to compaction/shake, which rebuild the cache anyway.
144
+ */
145
+ const PRUNE_CACHE_WARM_SUFFIX_TOKENS = 8_000;
146
+
147
+ /**
148
+ * Idle gap after which the supersede pass may flush the whole sent region (the
149
+ * provider cache is cold, so re-writing it is free). MUST exceed the maximum
150
+ * Anthropic prompt-cache TTL — "long" retention (the OAuth default) is 1h — or a
151
+ * still-warm prefix is busted by the flush. 90 min leaves margin over the 1h TTL.
152
+ */
153
+ const PRUNE_IDLE_FLUSH_MS = 90 * 60_000;
154
+
155
+ /**
156
+ * Hysteresis band for the post-maintenance "did we actually create headroom?"
157
+ * check shared by the shake tail and the context-full / snapcompact tail. A
158
+ * pass counts as having resolved threshold pressure only when residual context
159
+ * lands at or below `COMPACTION_RECOVERY_BAND × threshold`. Re-checking against
160
+ * the raw threshold lets a pass keep reclaiming a trickle of the previous
161
+ * turn's output and land just under the line every turn, sustaining the
162
+ * auto-continue dead loop reported in #2275; the same band stops the
163
+ * context-full / snapcompact tail from re-firing on a history whose single
164
+ * most-recent kept turn already exceeds the threshold (the snapcompact thrash).
165
+ */
166
+ const COMPACTION_RECOVERY_BAND = 0.8;
167
+
168
+ function mergeLlmCompactionPreserveData(
169
+ hookPreserveData: Record<string, unknown> | undefined,
170
+ resultPreserveData: Record<string, unknown> | undefined,
171
+ ): Record<string, unknown> | undefined {
172
+ const preserveData = { ...(hookPreserveData ?? {}), ...(resultPreserveData ?? {}) };
173
+ return snapcompact.stripPreservedArchive(Object.keys(preserveData).length > 0 ? preserveData : undefined);
174
+ }
175
+
176
+ /** Capabilities borrowed from the owning AgentSession. */
177
+ export interface SessionMaintenanceHost {
178
+ agent: Agent;
179
+ sessionManager: SessionManager;
180
+ settings: Settings;
181
+ modelRegistry: ModelRegistry;
182
+ extensionRunner: ExtensionRunner | undefined;
183
+ sideStreamFn: StreamFn;
184
+ providerSessionState: Map<string, ProviderSessionState>;
185
+ preferWebsockets: boolean | undefined;
186
+ model(): Model | undefined;
187
+ thinkingLevel(): ThinkingLevel | undefined;
188
+ isDisposed(): boolean;
189
+ isStreaming(): boolean;
190
+ isGeneratingHandoff(): boolean;
191
+ promptGeneration(): number;
192
+ sessionId(): string;
193
+ messages(): AgentMessage[];
194
+ baseSystemPrompt(): string[];
195
+ goalModeState(): GoalModeState | undefined;
196
+ planReferencePath(): string;
197
+ nonMessageTokenSource(): NonMessageTokenSource;
198
+ memoryBackendSession(): MemoryBackendOperationContext["session"];
199
+ emitSessionEvent(event: AgentSessionEvent): Promise<void>;
200
+ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
201
+ schedulePostPromptTask(
202
+ task: (signal: AbortSignal) => Promise<void>,
203
+ options?: { delayMs?: number; generation?: number; onSkip?: (reason: "aborted" | "stale-generation") => void },
204
+ ): void;
205
+ scheduleAgentContinue(options?: {
206
+ delayMs?: number;
207
+ generation?: number;
208
+ shouldContinue?: () => boolean;
209
+ onSkip?: (
210
+ reason:
211
+ | "aborted"
212
+ | "stale-generation"
213
+ | "session-unavailable"
214
+ | "should-continue-false"
215
+ | "post-restore-unavailable",
216
+ ) => void;
217
+ onError?: () => void;
218
+ }): void;
219
+ scheduleCompactionContinuation(options: {
220
+ generation: number;
221
+ autoContinue: boolean;
222
+ terminalTextAnswer: boolean;
223
+ suppressContinuation: boolean;
224
+ }): boolean;
225
+ persistTurnMessagesForMidRunCompaction(context: AgentTurnEndContext | undefined): Promise<boolean>;
226
+ findLastAssistantMessage(): AssistantMessage | undefined;
227
+ disconnectFromAgent(): void;
228
+ reconnectToAgent(): void;
229
+ drainStrandedQueuedMessages(): void;
230
+ buildDisplaySessionContext(): SessionContext;
231
+ convertToLlmForSideRequest(messages: AgentMessage[]): Message[];
232
+ obfuscateTextForProvider(text: string | undefined): string | undefined;
233
+ obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation;
234
+ closeCodexProviderSessionsForHistoryRewrite(): void;
235
+ resetCodexProviderAfterCompaction(compaction: CodexCompactionContext): void;
236
+ resetPlanReference(): void;
237
+ syncTodoPhasesFromBranch(): void;
238
+ resetAdvisorRuntimes(): void;
239
+ rebaseAfterCompaction(): void;
240
+ recordAnchoredHistoryRewrite(tokensRemoved: number): void;
241
+ getContextBreakdown(options?: {
242
+ contextWindow?: number;
243
+ pendingMessages?: AgentMessage[];
244
+ }): ContextUsageBreakdown | undefined;
245
+ getContextUsage(options?: { contextWindow?: number }): ContextUsage | undefined;
246
+ shake(mode: ShakeMode, options?: { config?: ShakeConfig; signal?: AbortSignal }): Promise<ShakeResult>;
247
+ dropImages(): Promise<{ removed: number }>;
248
+ runHandoff(customInstructions?: string, options?: SessionHandoffOptions): Promise<HandoffResult | undefined>;
249
+ removeAssistantMessageFromActiveContext(message: AssistantMessage): void;
250
+ dropPersistedAssistantTurn(message: AssistantMessage): Promise<void>;
251
+ runRecoveryCompactionWithRollback(
252
+ reason: "overflow" | "incomplete",
253
+ message: AssistantMessage,
254
+ allowDefer: boolean,
255
+ options: { autoContinue: boolean; triggerContextTokens?: number },
256
+ ): Promise<CompactionCheckResult>;
257
+ parseRetryAfterMsFromError(errorMessage: string): number | undefined;
258
+ setModelTemporary(
259
+ model: Model,
260
+ thinkingLevel?: ConfiguredThinkingLevel,
261
+ options?: { ephemeral?: boolean },
262
+ ): Promise<void>;
263
+ abort(options?: {
264
+ goalReason?: "interrupted" | "internal";
265
+ reason?: string;
266
+ preserveCompaction?: boolean;
267
+ }): Promise<void>;
268
+ abortHandoff(): void;
269
+ }
270
+
271
+ /** Owns compaction, pruning, shake, promotion, and automatic context maintenance. */
272
+ export class SessionMaintenance {
273
+ #compactionAbortController: AbortController | undefined;
274
+ #autoCompactionAbortController: AbortController | undefined;
275
+ /**
276
+ * Live tool-loop contexts parked after mid-turn maintenance hit a no-progress
277
+ * dead end. Membership suppresses the repeated rescue + warning while no cut
278
+ * point exists; {@link maintainContextMidRun} re-arms the entry once a later
279
+ * tool result makes `prepareCompaction` viable again.
280
+ */
281
+ readonly #midTurnCompactionDeadEnds = new WeakSet<AgentMessage[]>();
282
+ /**
283
+ * Carries a mid-turn dead end across the loop's final answer to the next
284
+ * pre-prompt check. That check must not warn again for the same oversized
285
+ * persisted turn, but a new agent loop still gets its own live-array guard.
286
+ */
287
+ #midTurnDeadEndPendingPrePrompt = false;
288
+ #skipPostTurnMaintenanceAssistantTimestamp: number | undefined;
289
+ readonly #host: SessionMaintenanceHost;
290
+
291
+ get #model(): Model | undefined {
292
+ return this.#host.model();
293
+ }
294
+
295
+ get #goalModeState(): GoalModeState | undefined {
296
+ return this.#host.goalModeState();
297
+ }
298
+
299
+ constructor(host: SessionMaintenanceHost) {
300
+ this.#host = host;
301
+ }
302
+
303
+ /** Whether manual or automatic context maintenance is active. */
304
+ get isCompacting(): boolean {
305
+ return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
306
+ }
307
+
308
+ /** Assistant timestamp whose post-turn maintenance must be skipped once. */
309
+ get skipPostTurnMaintenanceAssistantTimestamp(): number | undefined {
310
+ return this.#skipPostTurnMaintenanceAssistantTimestamp;
311
+ }
312
+
313
+ set skipPostTurnMaintenanceAssistantTimestamp(timestamp: number | undefined) {
314
+ this.#skipPostTurnMaintenanceAssistantTimestamp = timestamp;
315
+ }
316
+ /**
317
+ * Append plan-read protection to a prune/shake config so the active plan
318
+ * file survives compaction alongside skill reads (the config defaults
319
+ * already carry skill protection). The matcher reads the current plan
320
+ * reference path at match time, so retitled plans are covered.
321
+ */
322
+ #withPlanProtection<T extends { protectedTools: ProtectedToolMatcher[] }>(config: T): T {
323
+ const planMatcher = createPlanReadMatcher(() => this.#host.planReferencePath());
324
+ return { ...config, protectedTools: [...config.protectedTools, planMatcher] };
325
+ }
326
+
327
+ async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
328
+ const branchEntries = this.#host.sessionManager.getBranch();
329
+ const keepBoundaryId = getLatestCompactionEntry(branchEntries)?.firstKeptEntryId;
330
+ const result = pruneToolOutputs(
331
+ branchEntries,
332
+ this.#withPlanProtection({
333
+ ...DEFAULT_PRUNE_CONFIG,
334
+ pruneUseless: this.#host.settings.getGroup("compaction").dropUseless,
335
+ // Cache-stable boundary: never re-write the warm, already-sent prefix
336
+ // (deep stale/age victims) or summarized-away entries every turn.
337
+ keepBoundaryId,
338
+ cacheWarmSuffixTokens: PRUNE_CACHE_WARM_SUFFIX_TOKENS,
339
+ }),
340
+ );
341
+ if (result.prunedCount === 0) {
342
+ return undefined;
343
+ }
344
+
345
+ await this.#host.sessionManager.rewriteEntries();
346
+ const sessionContext = this.#host.buildDisplaySessionContext();
347
+ this.#host.agent.replaceMessages(sessionContext.messages);
348
+ this.#host.resetAdvisorRuntimes();
349
+ this.#host.syncTodoPhasesFromBranch();
350
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
351
+ return result;
352
+ }
353
+
354
+ /**
355
+ * Per-turn stale-result pass: prune older `read` results that a newer read
356
+ * of the same file has made stale, plus results their tool flagged
357
+ * contextually useless. Cache-aware (only fires when the suffix after a
358
+ * candidate is small or the session has been idle long enough that the
359
+ * provider prompt cache is cold), so it is cheap to run every turn. Gated
360
+ * on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
361
+ *
362
+ * Persists via `rewriteEntries` like every other history rewrite — the
363
+ * session file must match the live (pruned) context or file-based forks
364
+ * (`/fork`, `/tan`) and resume rebuild a divergent prefix and cold-miss the
365
+ * provider prompt cache.
366
+ */
367
+ async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
368
+ const { supersedeReads, dropUseless } = this.#host.settings.getGroup("compaction");
369
+ if (!supersedeReads && !dropUseless) return undefined;
370
+ const branchEntries = this.#host.sessionManager.getBranch();
371
+ const keepBoundaryId = getLatestCompactionEntry(branchEntries)?.firstKeptEntryId;
372
+ const result = pruneSupersededToolResults(
373
+ branchEntries,
374
+ this.#withPlanProtection({
375
+ supersedeKey: supersedeReads ? readToolSupersedeKey : undefined,
376
+ pruneUseless: dropUseless,
377
+ protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
378
+ // Never re-write summarized-away entries; only flush the whole sent
379
+ // region once the cache is genuinely cold (idle exceeds the 1h TTL).
380
+ keepBoundaryId,
381
+ idleFlushMs: PRUNE_IDLE_FLUSH_MS,
382
+ }),
383
+ );
384
+ if (result.prunedCount === 0) {
385
+ return undefined;
386
+ }
387
+
388
+ await this.#host.sessionManager.rewriteEntries();
389
+ const sessionContext = this.#host.buildDisplaySessionContext();
390
+ this.#host.agent.replaceMessages(sessionContext.messages);
391
+ this.#host.resetAdvisorRuntimes();
392
+ this.#host.syncTodoPhasesFromBranch();
393
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
394
+ return result;
395
+ }
396
+
397
+ /**
398
+ * Strip image content blocks from every message on the current branch and
399
+ * persist the rewrite. Walks `SessionManager.getBranch()` in place — both
400
+ * `SessionMessageEntry.message` and `CustomMessageEntry.content` arrays
401
+ * are mutated, then `rewriteEntries` durably commits the new shape. The
402
+ * agent's runtime view is rebuilt from the freshly-mutated entries so any
403
+ * provider sessions caching message identity (Codex Responses) are torn
404
+ * down to force a clean replay on the next turn.
405
+ *
406
+ * No-op when the branch carries no images; returns `{ removed: 0 }` and
407
+ * skips the disk rewrite.
408
+ */
409
+ async dropImages(): Promise<{ removed: number }> {
410
+ const branchEntries = this.#host.sessionManager.getBranch();
411
+ let removed = 0;
412
+ for (const entry of branchEntries) {
413
+ if (entry.type === "message") {
414
+ removed += stripImagesFromMessage(entry.message);
415
+ continue;
416
+ }
417
+ if (entry.type === "custom_message" && typeof entry.content !== "string") {
418
+ const kept: typeof entry.content = [];
419
+ let dropped = 0;
420
+ for (const part of entry.content) {
421
+ if (part.type === "image") {
422
+ dropped++;
423
+ } else {
424
+ kept.push(part);
425
+ }
426
+ }
427
+ if (dropped > 0) {
428
+ if (kept.length === 0) {
429
+ kept.push({ type: "text", text: "[image removed]" });
430
+ }
431
+ entry.content = kept;
432
+ removed += dropped;
433
+ }
434
+ }
435
+ }
436
+ if (removed === 0) {
437
+ return { removed: 0 };
438
+ }
439
+ await this.#host.sessionManager.rewriteEntries();
440
+ const sessionContext = this.#host.buildDisplaySessionContext();
441
+ this.#host.agent.replaceMessages(sessionContext.messages);
442
+ this.#host.resetAdvisorRuntimes();
443
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
444
+ return { removed };
445
+ }
446
+
447
+ /**
448
+ * Surgically reduce context by dropping heavy content ("shake").
449
+ *
450
+ * - `images` delegates to {@link dropImages}.
451
+ * - `elide` replaces whole tool-call results and large fenced/XML blocks
452
+ * with short placeholders that embed an `artifact://` recovery link.
453
+ *
454
+ * Mutates the branch in place, persists via `rewriteEntries`, replays the
455
+ * rebuilt context through the agent, and tears down provider sessions that
456
+ * cache message identity — same rewrite contract as {@link dropImages}.
457
+ *
458
+ * No-op (zero counts) when nothing is eligible.
459
+ */
460
+ async shake(mode: ShakeMode, opts: { config?: ShakeConfig; signal?: AbortSignal } = {}): Promise<ShakeResult> {
461
+ if (mode === "images") {
462
+ const { removed } = await this.#host.dropImages();
463
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, imagesDropped: removed, tokensFreed: 0 };
464
+ }
465
+
466
+ const branchEntries = this.#host.sessionManager.getBranch();
467
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
468
+ const config = this.#withPlanProtection({
469
+ ...(opts.config ?? AGGRESSIVE_SHAKE_CONFIG),
470
+ // Skip entries summarized away by the latest compaction — shaking them
471
+ // only churns persisted history with no prompt/cache effect.
472
+ keepBoundaryId: latestCompaction?.firstKeptEntryId,
473
+ });
474
+ const regions = collectShakeRegions(branchEntries, config);
475
+ if (regions.length === 0) {
476
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, tokensFreed: 0 };
477
+ }
478
+
479
+ const artifactId = await this.#saveShakeArtifact(regions);
480
+ const replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
481
+
482
+ const hasRemoteReplacementHistory = getOpenAiRemoteCompactionPayload(latestCompaction) !== undefined;
483
+ const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
484
+ let anchorIndex = -1;
485
+ for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
486
+ const entry = branchEntries[index];
487
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
488
+ const assistant = entry.message;
489
+ if (
490
+ assistant.stopReason !== "aborted" &&
491
+ assistant.stopReason !== "error" &&
492
+ assistant.usage &&
493
+ hasContextTokenUsage(assistant.usage)
494
+ ) {
495
+ anchorIndex = index;
496
+ break;
497
+ }
498
+ }
499
+ const entryIndexes = new Map(branchEntries.map((entry, index) => [entry, index]));
500
+
501
+ let toolResultsDropped = 0;
502
+ let blocksDropped = 0;
503
+ let originalTokens = 0;
504
+ let replacementTokens = 0;
505
+ let anchoredTokensRemoved = 0;
506
+ const items = regions.map((region, index) => {
507
+ if (region.kind === "toolResult") toolResultsDropped++;
508
+ else blocksDropped++;
509
+ originalTokens += region.tokens;
510
+ const replacement = replacements[index];
511
+ const replacementTokenCount = replacement.length > 0 ? countTokens(replacement) : 0;
512
+ replacementTokens += replacementTokenCount;
513
+ const entryIndex = entryIndexes.get(region.entry) ?? -1;
514
+ if (
515
+ entryIndex >= 0 &&
516
+ entryIndex < anchorIndex &&
517
+ (!hasRemoteReplacementHistory || entryIndex > compactionIndex)
518
+ ) {
519
+ anchoredTokensRemoved += Math.max(0, region.tokens - replacementTokenCount);
520
+ }
521
+ return { region, replacement };
522
+ });
523
+
524
+ applyShakeRegions(items);
525
+ this.#host.recordAnchoredHistoryRewrite(anchoredTokensRemoved);
526
+
527
+ await this.#host.sessionManager.rewriteEntries();
528
+ const sessionContext = this.#host.buildDisplaySessionContext();
529
+ this.#host.agent.replaceMessages(sessionContext.messages);
530
+ this.#host.resetAdvisorRuntimes();
531
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
532
+
533
+ return {
534
+ mode,
535
+ toolResultsDropped,
536
+ blocksDropped,
537
+ tokensFreed: Math.max(0, originalTokens - replacementTokens),
538
+ artifactId,
539
+ };
540
+ }
541
+
542
+ #shakeElidePlaceholder(region: ShakeRegion, index: number, artifactId: string | undefined): string {
543
+ if (artifactId) {
544
+ return `[shaken ~${region.tokens} tokens — recover: artifact://${artifactId} (region ${index + 1})]`;
545
+ }
546
+ return `[shaken ~${region.tokens} tokens]`;
547
+ }
548
+
549
+ /**
550
+ * Concatenate the original region contents into one session artifact so the
551
+ * agent can read them back via `artifact://<id>`. Returns `undefined` when
552
+ * the session is not persisted or the write fails — callers degrade to a
553
+ * bare placeholder.
554
+ */
555
+ async #saveShakeArtifact(regions: ShakeRegion[]): Promise<string | undefined> {
556
+ const parts: string[] = [];
557
+ for (let i = 0; i < regions.length; i++) {
558
+ const region = regions[i];
559
+ parts.push(`### region ${i + 1} (${region.label}, ~${region.tokens} tok)`, "", region.originalText, "");
560
+ }
561
+ try {
562
+ return await this.#host.sessionManager.saveArtifact(parts.join("\n"), "shake");
563
+ } catch {
564
+ return undefined;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Manually compact the session context.
570
+ * Aborts current agent operation first.
571
+ * @param customInstructions Optional instructions for the compaction summary
572
+ * @param options Optional callbacks for completion/error handling
573
+ */
574
+ async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
575
+ if (this.#compactionAbortController) {
576
+ throw new Error("Compaction already in progress");
577
+ }
578
+ // Resolve the `/compact <mode>` subcommand up front so input validation
579
+ // runs before we disconnect/abort the active agent operation below.
580
+ const compactMode = options?.mode ? findCompactMode(options.mode) : undefined;
581
+ // Modes that produce no LLM summary (snapcompact) have nothing to focus.
582
+ // Reject focus text loudly so programmatic callers don't silently lose
583
+ // instructions (the slash path pre-validates via parseCompactArgs).
584
+ // `internalGuidance` counts the same way — plan-mode approval never
585
+ // combines with a rejects-focus mode, but reject early if a caller ever
586
+ // wires it up so we don't silently drop the directive on the snapcompact
587
+ // fallback (issue #4359).
588
+ if (compactMode?.rejectsFocus && (customInstructions || options?.internalGuidance)) {
589
+ throw new Error(`/compact ${compactMode.name} does not take focus instructions.`);
590
+ }
591
+ const compactionAbortController = new AbortController();
592
+ this.#compactionAbortController = compactionAbortController;
593
+
594
+ try {
595
+ this.#host.disconnectFromAgent();
596
+ await this.#host.abort({ goalReason: "internal", preserveCompaction: true });
597
+ if (!this.#model) {
598
+ throw new Error("No model selected");
599
+ }
600
+
601
+ const compactionSettings = this.#host.settings.getGroup("compaction");
602
+ // The `/compact <mode>` override (resolved above) replaces the configured
603
+ // strategy/remote flags for this one invocation. Merged before
604
+ // prepareCompaction so the remote gating (preparation.settings.
605
+ // remoteEnabled/endpoint) and the snapcompact decision below both see it.
606
+ const effectiveSettings = compactMode
607
+ ? { ...compactionSettings, ...compactMode.overrides }
608
+ : compactionSettings;
609
+ // /compact remote demands provider-native compaction. When no remote
610
+ // endpoint is configured (one would override per-model gating in
611
+ // compact()), drop fallback candidates that aren't remote-capable so the
612
+ // engine never silently runs a local summary on a configured-but-non-
613
+ // remote compactionModel. If filtering empties the chain, warn and fall
614
+ // back to the full chain so the operation still completes.
615
+ const availableModels = this.#host.modelRegistry.getAvailable();
616
+ const requireProviderRemote = Boolean(compactMode?.requiresRemote && !effectiveSettings.remoteEndpoint);
617
+ let compactionCandidates = this.#getCompactionModelCandidates(
618
+ availableModels,
619
+ requireProviderRemote ? shouldUseOpenAiRemoteCompaction : undefined,
620
+ );
621
+ if (requireProviderRemote && compactionCandidates.length === 0) {
622
+ this.#host.emitNotice(
623
+ "warning",
624
+ `remote compaction is unavailable for ${this.#model.id} (no remote endpoint configured and no provider-native remote-capable model in the fallback chain) — using a local summary instead`,
625
+ "compaction",
626
+ );
627
+ compactionCandidates = this.#getCompactionModelCandidates(availableModels);
628
+ }
629
+ const pathEntries = this.#host.sessionManager.getBranch();
630
+ const preparation = prepareCompaction(pathEntries, effectiveSettings, this.#model);
631
+ if (!preparation) {
632
+ // Check why we can't compact
633
+ const lastEntry = pathEntries[pathEntries.length - 1];
634
+ if (lastEntry?.type === "compaction") {
635
+ throw new Error("Already compacted");
636
+ }
637
+ throw new Error("Nothing to compact (session too small)");
638
+ }
639
+
640
+ let hookCompaction: CompactionResult | undefined;
641
+ let fromExtension = false;
642
+ let preserveData: Record<string, unknown> | undefined;
643
+
644
+ if (this.#host.extensionRunner?.hasHandlers("session_before_compact")) {
645
+ const result = (await this.#host.extensionRunner.emit({
646
+ type: "session_before_compact",
647
+ preparation,
648
+ branchEntries: pathEntries,
649
+ customInstructions,
650
+ signal: compactionAbortController.signal,
651
+ })) as SessionBeforeCompactResult | undefined;
652
+
653
+ if (result?.cancel) {
654
+ throw new CompactionCancelledError();
655
+ }
656
+
657
+ if (result?.compaction) {
658
+ hookCompaction = result.compaction;
659
+ fromExtension = true;
660
+ }
661
+ }
662
+
663
+ const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
664
+
665
+ // Strategy honored on manual /compact too. Custom instructions (public
666
+ // user focus OR internal plan-mode guidance) imply a directed LLM
667
+ // summary; a text-only model cannot read snapcompact frames.
668
+ const wantsSnapcompact =
669
+ compactionPrep.kind !== "fromHook" &&
670
+ effectiveSettings.strategy === "snapcompact" &&
671
+ !customInstructions &&
672
+ !options?.internalGuidance;
673
+ // `/compact snapcompact` is an explicit no-LLM archive request: honor
674
+ // its contract by failing locally rather than silently shipping the
675
+ // transcript to a provider. The default-configured snapcompact
676
+ // strategy, in contrast, falls back to LLM compaction (mirroring the
677
+ // auto-compaction path) so a routine /compact still completes on a
678
+ // text-only model (issue #5064).
679
+ const explicitSnapcompact = compactMode?.name === "snapcompact";
680
+ let snapcompactReady = wantsSnapcompact;
681
+ const snapcompactShapeSetting = this.#host.settings.get("snapcompact.shape");
682
+ let snapcompactShape: snapcompact.Shape | undefined;
683
+ // Claude refuses inputs that reproduce its own reasoning as text
684
+ // ("reasoning_extraction"), and the snapcompact archive is replayed as
685
+ // text into every later request; drop `¶think:` sections for
686
+ // Anthropic-dialect targets (issue #6093).
687
+ const snapcompactIncludeThinking = preferredDialect(this.#model.id) !== "anthropic";
688
+ if (wantsSnapcompact && !this.#model.input.includes("image")) {
689
+ if (explicitSnapcompact) {
690
+ this.#host.emitNotice(
691
+ "warning",
692
+ `snapcompact needs a vision-capable model (${this.#model.id} is text-only)`,
693
+ "compaction",
694
+ );
695
+ throw new Error(`snapcompact cannot run locally: ${this.#model.id} is text-only.`);
696
+ }
697
+ this.#host.emitNotice(
698
+ "warning",
699
+ `snapcompact needs a vision-capable model (${this.#model.id} is text-only); falling back to LLM compaction`,
700
+ "compaction",
701
+ );
702
+ snapcompactReady = false;
703
+ } else if (snapcompactReady) {
704
+ const text = snapcompact.serializeConversation(
705
+ convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
706
+ { includeThinking: snapcompactIncludeThinking },
707
+ );
708
+ const probeText = snapcompact.renderabilityProbeText(
709
+ text,
710
+ preparation.previousPreserveData,
711
+ preparation.previousSummary,
712
+ );
713
+ snapcompactShape = snapcompact.resolveShapeForText(probeText, this.#model, snapcompactShapeSetting);
714
+ const renderScan = snapcompact.scanRenderability(probeText, { shape: snapcompactShape });
715
+ if (!renderScan.isSafe) {
716
+ const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
717
+ this.#host.emitNotice(
718
+ "warning",
719
+ `snapcompact disabled: unsupported characters for selected snapcompact font (${percent}%). No LLM fallback was attempted.`,
720
+ "compaction",
721
+ );
722
+ throw new Error(
723
+ `snapcompact cannot render this conversation locally: unsupported characters for selected snapcompact font (${percent}%).`,
724
+ );
725
+ }
726
+ }
727
+
728
+ let summary: string;
729
+ let shortSummary: string | undefined;
730
+ let firstKeptEntryId: string;
731
+ let tokensBefore: number;
732
+ let details: unknown;
733
+ let codexCompaction: CodexCompactionContext | undefined;
734
+
735
+ // Snapcompact runs locally first. The frame cap is sized from the live
736
+ // model window via #computeSnapcompactMaxFrames so the post-render context
737
+ // fits without the warning loop (issue #3247). Zero-frame budget now fails
738
+ // the snapcompact request locally rather than falling back to an LLM call.
739
+ let snapcompactResult: snapcompact.CompactionResult | undefined;
740
+ if (snapcompactReady) {
741
+ const maxFrames = this.#computeSnapcompactMaxFrames(preparation, effectiveSettings);
742
+ if (maxFrames < 1) {
743
+ logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
744
+ model: this.#model?.id,
745
+ });
746
+ this.#host.emitNotice(
747
+ "warning",
748
+ "snapcompact: kept history alone exceeds the context budget. No LLM fallback was attempted.",
749
+ "compaction",
750
+ );
751
+ throw new Error("snapcompact cannot run locally: kept history alone exceeds the context budget.");
752
+ } else {
753
+ const shape = snapcompactShape;
754
+ if (!shape) {
755
+ throw new Error("snapcompact shape was not resolved before rendering.");
756
+ }
757
+ snapcompactResult = await snapcompact.compact(preparation, {
758
+ convertToLlm,
759
+ model: this.#model,
760
+ ...(snapcompactShapeSetting === "auto" ? {} : { shape }),
761
+ maxFrames,
762
+ includeThinking: snapcompactIncludeThinking,
763
+ });
764
+ const framePayloadBytes = this.#snapcompactFramePayloadBytes(snapcompactResult);
765
+ if (framePayloadBytes > snapcompact.FRAME_DATA_BYTES_BUDGET) {
766
+ logger.warn("Snapcompact exceeded the per-request frame payload budget", {
767
+ model: this.#model?.id,
768
+ framePayloadBytes,
769
+ budget: snapcompact.FRAME_DATA_BYTES_BUDGET,
770
+ });
771
+ this.#host.emitNotice(
772
+ "warning",
773
+ "snapcompact produced too much standing image payload. No LLM fallback was attempted.",
774
+ "compaction",
775
+ );
776
+ throw new Error(
777
+ "snapcompact cannot run locally: standing image payload exceeds the per-request budget.",
778
+ );
779
+ }
780
+ const ctxWindow = this.#model?.contextWindow ?? 0;
781
+ const budget =
782
+ ctxWindow > 0
783
+ ? ctxWindow - effectiveReserveTokens(ctxWindow, effectiveSettings)
784
+ : Number.POSITIVE_INFINITY;
785
+ if (this.#projectSnapcompactContextTokens(preparation, snapcompactResult) > budget) {
786
+ logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
787
+ model: this.#model?.id,
788
+ });
789
+ this.#host.emitNotice(
790
+ "warning",
791
+ "snapcompact could not bring the context under the limit. No LLM fallback was attempted.",
792
+ "compaction",
793
+ );
794
+ throw new Error("snapcompact could not bring the context under the limit locally.");
795
+ }
796
+ }
797
+ }
798
+
799
+ if (compactionPrep.kind === "fromHook") {
800
+ summary = compactionPrep.summary;
801
+ shortSummary = compactionPrep.shortSummary;
802
+ firstKeptEntryId = compactionPrep.firstKeptEntryId;
803
+ tokensBefore = compactionPrep.tokensBefore;
804
+ details = compactionPrep.details;
805
+ preserveData = compactionPrep.preserveData;
806
+ } else if (snapcompactResult) {
807
+ summary = snapcompactResult.summary;
808
+ shortSummary = snapcompactResult.shortSummary;
809
+ firstKeptEntryId = snapcompactResult.firstKeptEntryId;
810
+ tokensBefore = snapcompactResult.tokensBefore;
811
+ details = snapcompactResult.details;
812
+ preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
813
+ } else {
814
+ codexCompaction = createCodexCompactionContext({
815
+ trigger: "manual",
816
+ reason: "user_requested",
817
+ phase: "standalone_turn",
818
+ });
819
+ // Generate compaction result. Only convert known abort-shaped
820
+ // rejections (AbortError raised while the abort signal is set,
821
+ // or an already-typed sentinel) into `CompactionCancelledError`
822
+ // so downstream callers can discriminate cancel from generic
823
+ // failure via `instanceof` without inspecting message strings.
824
+ // Real compaction bugs (network, server, parsing, etc.) keep
825
+ // their original shape — they must not be silently relabeled
826
+ // as cancellations even if the signal happens to be aborted
827
+ // for an unrelated reason. Assignments live inside the try
828
+ // block because every catch path throws — the post-try reads
829
+ // of the result-derived locals are reachable only on success.
830
+ try {
831
+ const result = await this.#compactWithFallbackModel(
832
+ preparation,
833
+ options?.internalGuidance ?? customInstructions,
834
+ compactionAbortController.signal,
835
+ {
836
+ promptOverride: this.#host.obfuscateTextForProvider(compactionPrep.hookPrompt),
837
+ extraContext: compactionPrep.hookContext,
838
+ remoteInstructions: this.#host.baseSystemPrompt().join("\n\n"),
839
+ convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
840
+ codexCompaction,
841
+ },
842
+ compactionCandidates,
843
+ );
844
+ summary = result.summary;
845
+ shortSummary = result.shortSummary;
846
+ firstKeptEntryId = result.firstKeptEntryId;
847
+ tokensBefore = result.tokensBefore;
848
+ details = result.details;
849
+ preserveData = mergeLlmCompactionPreserveData(compactionPrep.preserveData, result.preserveData);
850
+ } catch (err) {
851
+ if (err instanceof CompactionCancelledError) {
852
+ throw err;
853
+ }
854
+ if (compactionAbortController.signal.aborted && err instanceof Error && err.name === "AbortError") {
855
+ throw new CompactionCancelledError();
856
+ }
857
+ throw err;
858
+ }
859
+ }
860
+
861
+ if (compactionAbortController.signal.aborted) {
862
+ throw new CompactionCancelledError();
863
+ }
864
+
865
+ this.#host.sessionManager.appendCompaction(
866
+ summary,
867
+ shortSummary,
868
+ firstKeptEntryId,
869
+ tokensBefore,
870
+ details,
871
+ fromExtension,
872
+ preserveData,
873
+ );
874
+ const newEntries = this.#host.sessionManager.getEntries();
875
+ const sessionContext = this.#host.buildDisplaySessionContext();
876
+ this.#host.agent.replaceMessages(sessionContext.messages);
877
+ this.#host.rebaseAfterCompaction();
878
+ // Compaction discarded the conversation history that carried the approved
879
+ // plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
880
+ // the plan from disk and re-injects it on the next turn (issue #1246).
881
+ this.#host.resetPlanReference();
882
+ this.#host.resetAdvisorRuntimes();
883
+ this.#host.syncTodoPhasesFromBranch();
884
+ if (codexCompaction) {
885
+ this.#host.resetCodexProviderAfterCompaction(codexCompaction);
886
+ } else {
887
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
888
+ }
889
+
890
+ // Get the saved compaction entry for the hook
891
+ const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
892
+ | CompactionEntry
893
+ | undefined;
894
+
895
+ if (this.#host.extensionRunner && savedCompactionEntry) {
896
+ await this.#host.extensionRunner.emit({
897
+ type: "session_compact",
898
+ compactionEntry: savedCompactionEntry,
899
+ fromExtension,
900
+ });
901
+ }
902
+
903
+ const compactionResult: CompactionResult = {
904
+ summary,
905
+ shortSummary,
906
+ firstKeptEntryId,
907
+ tokensBefore,
908
+ details,
909
+ preserveData,
910
+ };
911
+ options?.onComplete?.(compactionResult);
912
+ return compactionResult;
913
+ } catch (error) {
914
+ const err = error instanceof Error ? error : new Error(String(error));
915
+ options?.onError?.(err);
916
+ throw error;
917
+ } finally {
918
+ if (this.#compactionAbortController === compactionAbortController) {
919
+ this.#compactionAbortController = undefined;
920
+ }
921
+ this.#host.reconnectToAgent();
922
+ // Compaction disconnected before `await abort()`, so abort's finally drain
923
+
924
+ // (and any steer/follow-up that arrived mid-compaction — async IRC, an
925
+ // `xd://` mount notice, an SDK/RPC steer) was suppressed while disconnected
926
+ // (issue #5800). Unlike `/new`/switchSession, compaction preserves the agent
927
+ // queues, so nothing else resumes them: re-drain now that the listener is back
928
+ // and `isCompacting` is false, or the queued turn hangs until the next prompt.
929
+ this.#host.drainStrandedQueuedMessages();
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Ask the active memory backend for an extra-context block to splice into
935
+ * the compaction summary prompt. Both the manual and auto compaction paths
936
+ * funnel through this helper so the behaviour stays identical.
937
+ *
938
+ * Failures are swallowed: a memory backend going sideways MUST NOT block
939
+ * compaction (which is itself the recovery path for context overflow).
940
+ */
941
+ async #collectMemoryBackendContext(preparation: {
942
+ messagesToSummarize: AgentMessage[];
943
+ turnPrefixMessages: AgentMessage[];
944
+ }): Promise<string | undefined> {
945
+ const backend = await resolveMemoryBackend(this.#host.settings);
946
+ if (!backend.preCompactionContext) return undefined;
947
+ const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
948
+ try {
949
+ return await backend.preCompactionContext(messages, this.#host.settings, this.#host.memoryBackendSession());
950
+ } catch (err) {
951
+ logger.debug("Memory backend preCompactionContext failed", {
952
+ backend: backend.id,
953
+ error: String(err),
954
+ });
955
+ return undefined;
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Cancel in-progress context maintenance (manual compaction, auto-compaction, or auto-handoff).
961
+ */
962
+ abortCompaction(): void {
963
+ this.#compactionAbortController?.abort();
964
+ this.#autoCompactionAbortController?.abort();
965
+ this.#host.abortHandoff();
966
+ }
967
+
968
+ /** Cancel only automatic maintenance while preserving a manual compaction. */
969
+ abortAutomaticCompaction(): void {
970
+ this.#autoCompactionAbortController?.abort();
971
+ }
972
+
973
+ /** Trigger idle compaction through the auto-compaction flow (with UI events). */
974
+ async runIdleCompaction(): Promise<void> {
975
+ if (this.#host.isStreaming() || this.isCompacting) return;
976
+ await this.runAutoCompaction("idle", false, true);
977
+ }
978
+
979
+ /**
980
+ * Local token estimate of the stored conversation (plus any pending messages),
981
+ * independent of provider-reported usage. A `before_provider_request` hook
982
+ * (e.g. a compression extension such as Headroom) or other on-wire payload
983
+ * transform can shrink the request below the real stored conversation; the
984
+ * provider then reports deflated prompt tokens, so anchoring the compaction
985
+ * decision purely on that usage lets the real history grow unbounded until it
986
+ * overflows and native compaction can no longer run. This estimate is the
987
+ * floor the compaction decision respects so on-wire compression can never
988
+ * suppress it.
989
+ */
990
+ #estimateStoredContextTokens(pendingMessages: AgentMessage[] = []): number {
991
+ // Exclude encrypted reasoning (thinkingSignature / redactedThinking): its
992
+ // local byte size diverges from what the provider bills, so counting it here
993
+ // would let a thinking-heavy turn falsely trip the floor. The provider usage
994
+ // (the other arm of compactionContextTokens) already accounts for it.
995
+ const opts = { excludeEncryptedReasoning: true } as const;
996
+ return (
997
+ computeNonMessageTokens(this.#host.nonMessageTokenSource()) +
998
+ this.#host.messages().reduce((sum, msg) => sum + estimateTokens(msg, opts), 0) +
999
+ pendingMessages.reduce((sum, msg) => sum + estimateTokens(msg, opts), 0)
1000
+ );
1001
+ }
1002
+
1003
+ #estimatePrePromptContextTokens(messages: AgentMessage[], contextWindow: number): number {
1004
+ const breakdown = this.#host.getContextBreakdown({ contextWindow, pendingMessages: messages });
1005
+ const localEstimate = this.#estimateStoredContextTokens(messages);
1006
+ // Floor by the local estimate: a payload-shrinking before_provider_request
1007
+ // hook deflates the provider-anchored breakdown, which must not suppress
1008
+ // pre-prompt compaction (see #estimateStoredContextTokens).
1009
+ return compactionContextTokens(breakdown?.usedTokens ?? 0, localEstimate);
1010
+ }
1011
+
1012
+ async runPrePromptCompactionIfNeeded(messages: AgentMessage[]): Promise<void> {
1013
+ const model = this.#model;
1014
+ if (!model) return;
1015
+ const contextWindow = model.contextWindow ?? 0;
1016
+ if (contextWindow <= 0) return;
1017
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1018
+ const contextTokens = this.#estimatePrePromptContextTokens(messages, contextWindow);
1019
+ const pendingMidTurnDeadEnd = this.#midTurnDeadEndPendingPrePrompt;
1020
+ this.#midTurnDeadEndPendingPrePrompt = false;
1021
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
1022
+ if (
1023
+ pendingMidTurnDeadEnd &&
1024
+ prepareCompaction(this.#host.sessionManager.getBranch(), compactionSettings, model) === undefined
1025
+ ) {
1026
+ // The prior tool loop already attempted the rescue and warned for this
1027
+ // persisted oversized turn. Only a later persisted cut point makes a
1028
+ // pre-prompt retry useful; the new agent loop may warn for its own turn.
1029
+ return;
1030
+ }
1031
+
1032
+ // Auto-promote first: switching to a larger-context model avoids compacting
1033
+ // the history at all. The post-turn threshold path already promotes before
1034
+ // compacting; without this, the pre-prompt path would pre-empt promotion and
1035
+ // compact (snapcompact/summary) a session that should have just been promoted.
1036
+ if (await this.#promoteContextModel()) {
1037
+ logger.debug("Pre-prompt context promotion avoided compaction", {
1038
+ contextTokens,
1039
+ contextWindow,
1040
+ model: `${model.provider}/${model.id}`,
1041
+ });
1042
+ return;
1043
+ }
1044
+
1045
+ logger.debug("Pre-prompt context maintenance triggered by pending prompt size", {
1046
+ contextTokens,
1047
+ contextWindow,
1048
+ model: `${model.provider}/${model.id}`,
1049
+ });
1050
+ await this.runAutoCompaction("threshold", false, false, false, {
1051
+ autoContinue: false,
1052
+ triggerContextTokens: contextTokens,
1053
+ phase: "pre_turn",
1054
+ });
1055
+ }
1056
+
1057
+ /**
1058
+ * Compact continuing tool-loop runs before the next provider request.
1059
+ *
1060
+ * `onTurnEnd` is the safe boundary: tool results for the just-finished turn
1061
+ * are already paired in `activeMessages`, the live array the agent loop reads
1062
+ * before its next model call. Before compacting, the just-finished turn is
1063
+ * synchronously persisted if async message hooks have not reached the normal
1064
+ * append path yet. Mid-run handoff is suppressed because resetting the session
1065
+ * while the loop owns `activeMessages` would race the next request; handoff
1066
+ * strategy falls back to in-place context-full compaction here.
1067
+ */
1068
+ async maintainContextMidRun(
1069
+ activeMessages: AgentMessage[],
1070
+ signal: AbortSignal | undefined,
1071
+ context: AgentTurnEndContext | undefined,
1072
+ ): Promise<void> {
1073
+ if (
1074
+ signal?.aborted ||
1075
+ this.#host.isDisposed() ||
1076
+ this.isCompacting ||
1077
+ this.#host.isGeneratingHandoff() ||
1078
+ !context?.willContinue
1079
+ )
1080
+ return;
1081
+
1082
+ const model = this.#model;
1083
+ const contextWindow = model?.contextWindow ?? 0;
1084
+ if (contextWindow <= 0) return;
1085
+
1086
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1087
+ if (
1088
+ !compactionSettings.enabled ||
1089
+ compactionSettings.strategy === "off" ||
1090
+ compactionSettings.midTurnEnabled === false
1091
+ ) {
1092
+ return;
1093
+ }
1094
+
1095
+ const lastAssistant = [...activeMessages]
1096
+ .reverse()
1097
+ .find((message): message is AssistantMessage => message.role === "assistant");
1098
+ if (!lastAssistant || lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") return;
1099
+
1100
+ if (!(await this.#host.persistTurnMessagesForMidRunCompaction(context))) return;
1101
+ if (this.#midTurnCompactionDeadEnds.has(activeMessages)) {
1102
+ // A prior boundary already ran the dead-end rescue and could not reduce
1103
+ // this turn. Re-running the rescue and re-emitting its warning on every
1104
+ // following tool boundary is wasted work while nothing summarizable
1105
+ // exists. But the tool loop keeps appending turns: once a later
1106
+ // (smaller) tool result gives prepareCompaction a cut point before the
1107
+ // now-older oversized turn, compaction can finally make progress and
1108
+ // MUST run rather than stay suppressed until provider overflow (#7153
1109
+ // review). Stay parked only while no cut point is available; re-arm as
1110
+ // soon as one appears.
1111
+ if (
1112
+ !model ||
1113
+ prepareCompaction(this.#host.sessionManager.getBranch(), compactionSettings, model) === undefined
1114
+ ) {
1115
+ return;
1116
+ }
1117
+ this.#midTurnCompactionDeadEnds.delete(activeMessages);
1118
+ this.#midTurnDeadEndPendingPrePrompt = false;
1119
+ }
1120
+
1121
+ const billedContextTokens = calculateContextTokens(lastAssistant.usage);
1122
+ const storedContextTokens = this.#estimateStoredContextTokens();
1123
+ const contextTokens = compactionContextTokens(billedContextTokens, storedContextTokens);
1124
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
1125
+
1126
+ // Promote to a larger-context sibling before compacting, mirroring the
1127
+ // pre-prompt (runPrePromptCompactionIfNeeded) and post-turn threshold
1128
+ // (checkCompaction) paths. Without this, a long mid-turn tool loop that
1129
+ // crosses the threshold compacts the history (and can hit the no-progress
1130
+ // dead-end on a single oversized turn) on a model that should have just
1131
+ // been promoted to a larger window instead.
1132
+ if (await this.#promoteContextModel()) {
1133
+ logger.debug("Mid-run context promotion avoided compaction", {
1134
+ contextTokens,
1135
+ contextWindow,
1136
+ from: `${model?.provider}/${model?.id}`,
1137
+ });
1138
+ return;
1139
+ }
1140
+
1141
+ const messagesBefore = activeMessages.length;
1142
+ const result = await this.runAutoCompaction("threshold", false, false, false, {
1143
+ autoContinue: false,
1144
+ suppressContinuation: true,
1145
+ suppressHandoff: true,
1146
+ triggerContextTokens: contextTokens,
1147
+ phase: "mid_turn",
1148
+ });
1149
+ if (result.automaticContinuationBlocked) {
1150
+ this.#midTurnCompactionDeadEnds.add(activeMessages);
1151
+ this.#midTurnDeadEndPendingPrePrompt = true;
1152
+ }
1153
+
1154
+ if (signal?.aborted) return;
1155
+ const compactedMessages = this.#host.agent.state.messages;
1156
+ if (compactedMessages !== activeMessages) {
1157
+ activeMessages.splice(0, activeMessages.length, ...compactedMessages);
1158
+ }
1159
+ logger.debug("Mid-run compaction ran between provider calls", {
1160
+ contextTokens,
1161
+ contextWindow,
1162
+ strategy: compactionSettings.strategy,
1163
+ goalActive: this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active",
1164
+ messagesBefore,
1165
+ messagesAfter: activeMessages.length,
1166
+ });
1167
+ }
1168
+ /**
1169
+ * Check if context maintenance or promotion is needed and run it.
1170
+ * Called after agent_end and before prompt submission.
1171
+ *
1172
+ * Four cases (in order):
1173
+ * 1. Input overflow + promotion: promote to larger model, retry without maintenance.
1174
+ * 2. Input overflow + no promotion target: run context maintenance, auto-retry on same model.
1175
+ * 3. Output incomplete (stopReason === "length", e.g. `response.incomplete`): the
1176
+ * model burned its output budget without producing an actionable deliverable
1177
+ * (reasoning-only or truncated). Drop the dead turn, try promotion, otherwise
1178
+ * run compaction/handoff and retry.
1179
+ * 4. Threshold: context over threshold, run context maintenance (no auto-retry).
1180
+ *
1181
+ * @param assistantMessage The assistant message to check
1182
+ * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
1183
+ * @param allowDefer If true, threshold-driven handoff strategy may schedule itself as a
1184
+ * deferred post-prompt task instead of running inline. Callers running inside the
1185
+ * `agent_end` handler set this to true so `session.prompt()` resolves cleanly; callers
1186
+ * on the pre-prompt path (where the next agent turn is about to start) set it to false
1187
+ * to avoid racing the deferred handoff against the new turn.
1188
+ * @param autoContinue Whether maintenance may schedule the agent-authored continuation prompt.
1189
+ * @returns whether compaction/recovery scheduled a handoff, retry, auto-continue, or
1190
+ * queued-message drain that already owns the next turn. Callers MUST skip
1191
+ * `session_stop` and other agent continuations when `continuationScheduled`
1192
+ * is true.
1193
+ */
1194
+ async checkCompaction(
1195
+ assistantMessage: AssistantMessage,
1196
+ skipAbortedCheck = true,
1197
+ allowDefer = true,
1198
+ autoContinue = true,
1199
+ ): Promise<CompactionCheckResult> {
1200
+ // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
1201
+ if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return COMPACTION_CHECK_NONE;
1202
+ const contextWindow = this.#model?.contextWindow ?? 0;
1203
+ const generation = this.#host.promptGeneration();
1204
+ // Skip overflow check if the message came from a different model.
1205
+ // This handles the case where user switched from a smaller-context model (e.g. opus)
1206
+ // to a larger-context model (e.g. codex) - the overflow error from the old model
1207
+ // shouldn't trigger compaction for the new model.
1208
+ const sameModel =
1209
+ this.#model && assistantMessage.provider === this.#model.provider && assistantMessage.model === this.#model.id;
1210
+ // This handles the case where an error was kept after compaction (in the "kept" region).
1211
+ // The error shouldn't trigger another compaction since we already compacted.
1212
+ // Example: opus fails -> switch to codex -> compact -> switch back to opus -> opus error
1213
+ // is still in context but shouldn't trigger compaction again.
1214
+ const compactionEntry = getLatestCompactionEntry(this.#host.sessionManager.getBranch());
1215
+ const errorIsFromBeforeCompaction =
1216
+ compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
1217
+ if (sameModel && !errorIsFromBeforeCompaction && AIError.isContextOverflow(assistantMessage, contextWindow)) {
1218
+ // Clear the failed turn from active context so the retry (or the next
1219
+ // user prompt) does not replay it. The persisted branch entry stays
1220
+ // for now: when no recovery path runs, the user-facing transcript
1221
+ // MUST keep the only assistant message explaining why the turn
1222
+ // stopped. The branch entry is dropped further down, but only on the
1223
+ // paths that actually schedule a retry/compaction.
1224
+ this.#host.removeAssistantMessageFromActiveContext(assistantMessage);
1225
+
1226
+ // Try context promotion first - switch to a larger model and retry without compacting
1227
+ const promoted = await this.#tryContextPromotion(assistantMessage);
1228
+ if (promoted) {
1229
+ await this.#host.dropPersistedAssistantTurn(assistantMessage);
1230
+ // Retry on the promoted (larger) model without compacting
1231
+ this.#host.scheduleAgentContinue({ delayMs: 100, generation });
1232
+ return COMPACTION_CHECK_CONTINUATION;
1233
+ }
1234
+
1235
+ // No promotion target available fall through to compaction
1236
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1237
+ if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
1238
+ return await this.#host.runRecoveryCompactionWithRollback("overflow", assistantMessage, allowDefer, {
1239
+ autoContinue,
1240
+ });
1241
+ }
1242
+ return COMPACTION_CHECK_NONE;
1243
+ }
1244
+ // A context promotion can land while the failing call is already in
1245
+ // flight (or on a run whose loop predates the switch): the overflow
1246
+ // error then arrives stamped with the pre-promotion model while
1247
+ // `this.#host.model()` is already the promoted target. The sameModel guard
1248
+ // above deliberately ignores stale foreign-model errors, but this
1249
+ // state is not stale — recover exactly like the promotion path:
1250
+ // drop the dead turn and retry on the already-promoted model. Gated
1251
+ // narrowly on "current model IS the failed model's promotion target
1252
+ // with a strictly larger window" so genuinely stale errors from
1253
+ // old user-switched models keep surfacing untouched.
1254
+ if (
1255
+ !sameModel &&
1256
+ autoContinue &&
1257
+ !errorIsFromBeforeCompaction &&
1258
+ assistantMessage.stopReason === "error" &&
1259
+ this.#model &&
1260
+ contextWindow > 0 &&
1261
+ this.#host.settings.getGroup("contextPromotion").enabled
1262
+ ) {
1263
+ const failedModel = this.#host.modelRegistry.find(assistantMessage.provider, assistantMessage.model);
1264
+ const failedWindow = failedModel?.contextWindow ?? 0;
1265
+ const promotionTarget = failedModel
1266
+ ? resolveContextPromotionConfiguredTarget(failedModel, this.#host.modelRegistry.getAvailable())
1267
+ : undefined;
1268
+ if (
1269
+ failedModel &&
1270
+ failedWindow > 0 &&
1271
+ contextWindow > failedWindow &&
1272
+ promotionTarget &&
1273
+ modelsAreEqual(promotionTarget, this.#model) &&
1274
+ AIError.isContextOverflow(assistantMessage, failedWindow)
1275
+ ) {
1276
+ this.#host.removeAssistantMessageFromActiveContext(assistantMessage);
1277
+ await this.#host.dropPersistedAssistantTurn(assistantMessage);
1278
+ logger.debug("Overflow on pre-promotion model; retrying on promoted model", {
1279
+ failed: `${assistantMessage.provider}/${assistantMessage.model}`,
1280
+ current: `${this.#model.provider}/${this.#model.id}`,
1281
+ });
1282
+ this.#host.scheduleAgentContinue({ delayMs: 100, generation });
1283
+ return COMPACTION_CHECK_CONTINUATION;
1284
+ }
1285
+ }
1286
+
1287
+ // Case 3: Output-side incomplete — `response.incomplete` from OpenAI Responses
1288
+ // (and Codex) maps to stopReason === "length". The model burned its
1289
+ // `max_output_tokens` budget on reasoning/text and emitted no actionable
1290
+ // deliverable. Same recovery class as overflow: promotion if available,
1291
+ // otherwise compaction/handoff. Unlike overflow, the *input* is fine, so we
1292
+ // allow the handoff strategy to actually run.
1293
+ if (sameModel && !errorIsFromBeforeCompaction && assistantMessage.stopReason === "length") {
1294
+ // Same active-context vs persisted-history split as the overflow path
1295
+ // above: clear the dead turn from agent state so it cannot be replayed,
1296
+ // but keep it on the branch unless promotion or compaction actually runs.
1297
+ this.#host.removeAssistantMessageFromActiveContext(assistantMessage);
1298
+
1299
+ const promoted = await this.#tryContextPromotion(assistantMessage);
1300
+ if (promoted) {
1301
+ await this.#host.dropPersistedAssistantTurn(assistantMessage);
1302
+ logger.debug("Context promotion triggered by response.incomplete (length stop)", {
1303
+ from: `${assistantMessage.provider}/${assistantMessage.model}`,
1304
+ });
1305
+ this.#host.scheduleAgentContinue({ delayMs: 100, generation });
1306
+ return COMPACTION_CHECK_CONTINUATION;
1307
+ }
1308
+
1309
+ const incompleteCompactionSettings = this.#host.settings.getGroup("compaction");
1310
+ if (incompleteCompactionSettings.enabled && incompleteCompactionSettings.strategy !== "off") {
1311
+ logger.debug("Compaction triggered by response.incomplete (length stop, no promotion target)", {
1312
+ model: `${assistantMessage.provider}/${assistantMessage.model}`,
1313
+ strategy: incompleteCompactionSettings.strategy,
1314
+ });
1315
+ return await this.#host.runRecoveryCompactionWithRollback("incomplete", assistantMessage, allowDefer, {
1316
+ autoContinue,
1317
+ triggerContextTokens: calculateContextTokens(assistantMessage.usage),
1318
+ });
1319
+ }
1320
+ // Neither promotion nor compaction is available — surface the dead-end so
1321
+ // the user understands why the turn yielded with nothing.
1322
+ logger.warn("response.incomplete with no recovery path (promotion + compaction both unavailable)", {
1323
+ model: `${assistantMessage.provider}/${assistantMessage.model}`,
1324
+ });
1325
+ return COMPACTION_CHECK_NONE;
1326
+ }
1327
+
1328
+ // Stale-result pass runs every turn, before any threshold gating: it is
1329
+ // cheap (bails when no candidate) and independent of the compaction
1330
+ // setting.
1331
+ const supersedeResult = await this.#pruneStaleToolResults();
1332
+
1333
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1334
+ if (!compactionSettings.enabled || compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
1335
+
1336
+ // Case 4: Threshold - turn succeeded but context is getting large
1337
+ // Skip if this was an error (non-overflow errors don't have usage data)
1338
+ if (assistantMessage.stopReason === "error") return COMPACTION_CHECK_NONE;
1339
+ const pruneResult = await this.#pruneToolOutputs();
1340
+ const maintenanceTokensFreed = (supersedeResult?.tokensSaved ?? 0) + (pruneResult?.tokensSaved ?? 0);
1341
+ // `errorIsFromBeforeCompaction` (computed above) is the general
1342
+ // "this assistant message predates the latest compaction" predicate here,
1343
+ // not just an error-specific one; alias it locally so the threshold intent
1344
+ // reads clearly (#3412 review).
1345
+ const assistantPredatesCompaction = errorIsFromBeforeCompaction;
1346
+ // An assistant that predates the latest compaction carries stale, pre-rewrite
1347
+ // `usage`: the scheduled auto-continue re-enters this check with the kept
1348
+ // assistant (#promptWithMessage → checkCompaction), and its old high prompt
1349
+ // count would re-trip the threshold on a freshly compacted history. Drop the
1350
+ // stale provider number for those messages and let the live stored estimate
1351
+ // (the floor applied below) drive the decision instead.
1352
+ const assistantUsageContextTokens = assistantPredatesCompaction
1353
+ ? 0
1354
+ : calculateContextTokens(assistantMessage.usage);
1355
+ const storedContextTokens = this.#estimateStoredContextTokens();
1356
+ // Pruning frees bytes for the NEXT prompt; it does not change the size of
1357
+ // the prompt the LLM just billed for. Earlier revisions subtracted the
1358
+ // per-turn supersede/prune `tokensSaved` from the threshold input, which
1359
+ // let a long-running `/goal` session sit above `compaction.thresholdTokens`
1360
+ // indefinitely whenever per-turn pruning saved enough to drop the
1361
+ // post-prune estimate below the user-configured trigger — the visible
1362
+ // context (anchored to the same provider billing) still showed >threshold,
1363
+ // but `shouldCompact` no-op'd (#3174). Anchor the initial trigger on the
1364
+ // last turn's billed context tokens, floored by the post-prune
1365
+ // stored-conversation estimate so a payload-compression hook still can't
1366
+ // deflate the trigger.
1367
+ const contextTokens = compactionContextTokens(assistantUsageContextTokens, storedContextTokens);
1368
+ const postMaintenanceContextTokens = compactionContextTokens(
1369
+ Math.max(0, assistantUsageContextTokens - maintenanceTokensFreed),
1370
+ storedContextTokens,
1371
+ );
1372
+ const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
1373
+ const shouldThresholdCompact = shouldCompact(contextTokens, contextWindow, compactionSettings);
1374
+ logger.debug("Auto-compaction threshold decision", {
1375
+ phase: "post-agent-end",
1376
+ goalModeEnabled: this.#goalModeState?.enabled === true,
1377
+ goalStatus: this.#goalModeState?.goal.status,
1378
+ stopReason: assistantMessage.stopReason,
1379
+ sameModel: sameModel === true,
1380
+ contextWindow,
1381
+ strategy: compactionSettings.strategy,
1382
+ thresholdTokens,
1383
+ assistantUsageContextTokens,
1384
+ storedContextTokens,
1385
+ resolvedContextTokens: contextTokens,
1386
+ postMaintenanceContextTokens,
1387
+ maintenanceTokensFreed,
1388
+ shouldCompact: shouldThresholdCompact,
1389
+ contextPromotionEnabled: this.#host.settings.get("contextPromotion.enabled") === true,
1390
+ });
1391
+ if (shouldThresholdCompact) {
1392
+ // Try promotion first — if a larger model is available, switch instead of compacting
1393
+ const promoted = await this.#tryContextPromotion(assistantMessage);
1394
+ if (!promoted) {
1395
+ return await this.runAutoCompaction("threshold", false, false, allowDefer, {
1396
+ autoContinue,
1397
+ triggerContextTokens: postMaintenanceContextTokens,
1398
+ phase: "pre_turn",
1399
+ terminalTextAnswer: isTerminalTextAssistantAnswer(assistantMessage),
1400
+ });
1401
+ }
1402
+ logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
1403
+ contextTokens,
1404
+ contextWindow,
1405
+ model: `${assistantMessage.provider}/${assistantMessage.model}`,
1406
+ });
1407
+ }
1408
+ return COMPACTION_CHECK_NONE;
1409
+ }
1410
+
1411
+ /**
1412
+ * Attempt context promotion to a larger model.
1413
+ * Returns true if promotion succeeded (caller should retry without compacting).
1414
+ */
1415
+ async #tryContextPromotion(assistantMessage: AssistantMessage): Promise<boolean> {
1416
+ const currentModel = this.#model;
1417
+ if (!currentModel) return false;
1418
+ // The overflow/length error may have come from a model the user already
1419
+ // switched away from; only promote when the failing turn was this model.
1420
+ if (assistantMessage.provider !== currentModel.provider || assistantMessage.model !== currentModel.id)
1421
+ return false;
1422
+ return this.#promoteContextModel();
1423
+ }
1424
+
1425
+ /**
1426
+ * Switch to a larger-context sibling when context promotion is enabled and a
1427
+ * target with a strictly larger window (and a usable key) exists. Returns true
1428
+ * when the model was switched, so the caller can retry without compacting.
1429
+ * Message-independent core shared by the post-turn overflow path
1430
+ * ({@link #tryContextPromotion}) and the pre-prompt threshold path
1431
+ * ({@link runPrePromptCompactionIfNeeded}).
1432
+ */
1433
+ async #promoteContextModel(): Promise<boolean> {
1434
+ const promotionSettings = this.#host.settings.getGroup("contextPromotion");
1435
+ if (!promotionSettings.enabled) return false;
1436
+ const currentModel = this.#model;
1437
+ if (!currentModel) return false;
1438
+ const contextWindow = currentModel.contextWindow ?? 0;
1439
+ if (contextWindow <= 0) return false;
1440
+ const targetModel = await this.resolveContextPromotionTarget(currentModel, contextWindow);
1441
+ if (!targetModel) return false;
1442
+
1443
+ try {
1444
+ await this.#host.setModelTemporary(targetModel, undefined, { ephemeral: true });
1445
+ logger.debug("Context promotion switched model on overflow", {
1446
+ from: `${currentModel.provider}/${currentModel.id}`,
1447
+ to: `${targetModel.provider}/${targetModel.id}`,
1448
+ });
1449
+ return true;
1450
+ } catch (error) {
1451
+ logger.warn("Context promotion failed", {
1452
+ from: `${currentModel.provider}/${currentModel.id}`,
1453
+ to: `${targetModel.provider}/${targetModel.id}`,
1454
+ error: String(error),
1455
+ });
1456
+ return false;
1457
+ }
1458
+ }
1459
+
1460
+ async resolveContextPromotionTarget(
1461
+ currentModel: Model,
1462
+ contextWindow: number,
1463
+ signal?: AbortSignal,
1464
+ ): Promise<Model | undefined> {
1465
+ const availableModels = this.#host.modelRegistry.getAvailable();
1466
+ if (availableModels.length === 0) return undefined;
1467
+
1468
+ const candidate = resolveContextPromotionConfiguredTarget(currentModel, availableModels);
1469
+ if (!candidate) return undefined;
1470
+ if (modelsAreEqual(candidate, currentModel)) return undefined;
1471
+ if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
1472
+ const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId(), { signal });
1473
+ if (!apiKey) return undefined;
1474
+ return candidate;
1475
+ }
1476
+
1477
+ #getCompactionModelCandidates(availableModels: Model[], filter?: (model: Model) => boolean): Model[] {
1478
+ return this.resolveCompactionModelCandidates(this.#model, availableModels, filter);
1479
+ }
1480
+
1481
+ resolveCompactionModelCandidates(
1482
+ preferredModel: Model | null | undefined,
1483
+ availableModels: Model[],
1484
+ filter?: (model: Model) => boolean,
1485
+ ): Model[] {
1486
+ const candidates: Model[] = [];
1487
+ const seen = new Set<string>();
1488
+
1489
+ const addCandidate = (model: Model | undefined): void => {
1490
+ if (!model) return;
1491
+ const key = `${model.provider}/${model.id}`;
1492
+ if (seen.has(key)) return;
1493
+ seen.add(key);
1494
+ // `seen` still tracks rejected models so the largest-context fallback
1495
+ // scan below doesn't reintroduce them; the filter just suppresses
1496
+ // inclusion in this caller's candidate chain.
1497
+ if (filter && !filter(model)) return;
1498
+ candidates.push(model);
1499
+ };
1500
+
1501
+ if (preferredModel) {
1502
+ addCandidate(resolveCompactionConfiguredTarget(preferredModel, availableModels));
1503
+ }
1504
+ addCandidate(preferredModel ?? undefined);
1505
+ for (const role of MODEL_ROLE_IDS) {
1506
+ addCandidate(
1507
+ resolveRoleModelFull(this.#host.settings, role, availableModels, preferredModel ?? undefined).model,
1508
+ );
1509
+ }
1510
+
1511
+ const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
1512
+ for (const model of sortedByContext) {
1513
+ if (!seen.has(`${model.provider}/${model.id}`)) {
1514
+ addCandidate(model);
1515
+ break;
1516
+ }
1517
+ }
1518
+
1519
+ return candidates;
1520
+ }
1521
+
1522
+ #buildCompactionAuthError(): Error {
1523
+ const currentModel = this.#model;
1524
+ if (!currentModel) {
1525
+ return new Error(
1526
+ "Compaction requires a model with usable credentials, but no authenticated compaction model is available.",
1527
+ );
1528
+ }
1529
+ return new Error(
1530
+ `Compaction requires usable credentials for ${currentModel.provider}/${currentModel.id}. ` +
1531
+ `Configure ${currentModel.provider} credentials or assign an authenticated fallback role such as modelRoles.smol.`,
1532
+ );
1533
+ }
1534
+
1535
+ async #compactWithFallbackModel(
1536
+ preparation: CompactionPreparation,
1537
+ customInstructions: string | undefined,
1538
+ signal: AbortSignal,
1539
+ options?: SummaryOptions,
1540
+ precomputedCandidates?: Model[],
1541
+ ): Promise<CompactionResult> {
1542
+ const candidates =
1543
+ precomputedCandidates ?? this.#getCompactionModelCandidates(this.#host.modelRegistry.getAvailable());
1544
+ const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
1545
+ let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
1546
+
1547
+ for (const candidate of candidates) {
1548
+ const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
1549
+ if (!apiKey) continue;
1550
+ if (
1551
+ nativeCompactionFailure &&
1552
+ (candidate.provider !== nativeCompactionFailure.provider ||
1553
+ !shouldUseProviderNativeCompaction(candidate, preparation.settings))
1554
+ ) {
1555
+ throw nativeCompactionFailure.error;
1556
+ }
1557
+
1558
+ try {
1559
+ return await compact(
1560
+ this.#host.obfuscatePreparationForProvider(preparation),
1561
+ candidate,
1562
+ this.#host.modelRegistry.resolver(candidate, this.#host.sessionId()),
1563
+ this.#host.obfuscateTextForProvider(customInstructions),
1564
+ signal,
1565
+ {
1566
+ ...options,
1567
+ metadata: this.#host.agent.metadataForProvider(candidate.provider),
1568
+ convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
1569
+ telemetry,
1570
+ // Honor the user's /model thinking selection (incl. `off`) on
1571
+ // the manual `/compact` path. Clamped per-model inside compact()
1572
+ // via resolveCompactionEffort so unsupported-effort models
1573
+ // (xai-oauth/grok-build) don't trip requireSupportedEffort.
1574
+ thinkingLevel: this.#host.thinkingLevel(),
1575
+ tools: this.#host.agent.state.tools,
1576
+ sessionId: this.#host.sessionId(),
1577
+ promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
1578
+ providerSessionState: this.#host.providerSessionState,
1579
+ preferWebsockets: this.#host.preferWebsockets,
1580
+ // Route every summarization HTTP request through the
1581
+ // session's side-stream transport so the provider
1582
+ // concurrency cap (e.g. providers.ollama-cloud.maxConcurrency)
1583
+ // brackets compaction the same way it brackets the live
1584
+ // agent turn — without this, multiple ollama-cloud
1585
+ // subagents auto/manually compacting issued uncapped
1586
+ // summary requests in parallel (chatgpt-codex review on
1587
+ // #3751).
1588
+ completeImpl: async (requestModel, requestContext, requestOptions) => {
1589
+ const stream = await this.#host.sideStreamFn(requestModel, requestContext, requestOptions);
1590
+ return stream.result();
1591
+ },
1592
+ },
1593
+ );
1594
+ } catch (error) {
1595
+ const id = AIError.classify(error instanceof NativeCompactionError ? error.cause : error, candidate.api);
1596
+ if (AIError.is(id, AIError.Flag.AuthFailed)) continue;
1597
+ if (error instanceof NativeCompactionError) {
1598
+ nativeCompactionFailure ??= { error, provider: candidate.provider };
1599
+ continue;
1600
+ }
1601
+ throw error;
1602
+ }
1603
+ }
1604
+
1605
+ if (nativeCompactionFailure) throw nativeCompactionFailure.error;
1606
+ throw this.#buildCompactionAuthError();
1607
+ }
1608
+
1609
+ async #prepareCompactionFromHooks(
1610
+ preparation: CompactionPreparation,
1611
+ hookCompaction: CompactionResult | undefined,
1612
+ ): Promise<
1613
+ | {
1614
+ kind: "fromHook";
1615
+ summary: string;
1616
+ shortSummary: string | undefined;
1617
+ firstKeptEntryId: string;
1618
+ tokensBefore: number;
1619
+ details: unknown;
1620
+ preserveData: Record<string, unknown> | undefined;
1621
+ }
1622
+ | {
1623
+ kind: "needsLlm";
1624
+ hookContext: string[] | undefined;
1625
+ hookPrompt: string | undefined;
1626
+ preserveData: Record<string, unknown> | undefined;
1627
+ }
1628
+ > {
1629
+ let hookContext: string[] | undefined;
1630
+ let hookPrompt: string | undefined;
1631
+ let preserveData: Record<string, unknown> | undefined;
1632
+
1633
+ if (!hookCompaction && this.#host.extensionRunner?.hasHandlers("session.compacting")) {
1634
+ const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
1635
+ const result = (await this.#host.extensionRunner.emit({
1636
+ type: "session.compacting",
1637
+ sessionId: this.#host.sessionId(),
1638
+ messages: compactMessages,
1639
+ })) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
1640
+
1641
+ hookContext = result?.context;
1642
+ hookPrompt = result?.prompt;
1643
+ preserveData = result?.preserveData;
1644
+ }
1645
+
1646
+ const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
1647
+ if (memoryBackendContext) {
1648
+ hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
1649
+ }
1650
+
1651
+ if (hookCompaction) {
1652
+ preserveData ??= hookCompaction.preserveData;
1653
+ return {
1654
+ kind: "fromHook",
1655
+ summary: hookCompaction.summary,
1656
+ shortSummary: hookCompaction.shortSummary,
1657
+ firstKeptEntryId: hookCompaction.firstKeptEntryId,
1658
+ tokensBefore: hookCompaction.tokensBefore,
1659
+ details: hookCompaction.details,
1660
+ preserveData,
1661
+ };
1662
+ }
1663
+
1664
+ return { kind: "needsLlm", hookContext, hookPrompt, preserveData };
1665
+ }
1666
+
1667
+ /**
1668
+ * Cap on snapcompact frames the post-compaction context can carry without
1669
+ * busting the model window. Mirrors the per-frame token charge used by the
1670
+ * projection ({@link snapcompact.FRAME_TOKEN_ESTIMATE}, the conservative
1671
+ * high-res Anthropic ceiling), so picking `maxFrames` from this helper makes
1672
+ * {@link #projectSnapcompactContextTokens} succeed by construction.
1673
+ *
1674
+ * Skip vs. cap use different reserves on purpose. The **skip** decision
1675
+ * (return `0`) trips only when kept-recent plus non-message tokens already
1676
+ * eat the entire `ctxWindow − reserve` envelope: at that point no archive
1677
+ * shape — frame-bearing or text-only — can fit, and the caller MUST
1678
+ * shortcut to the LLM summarizer instead of re-running snapcompact to
1679
+ * re-emit the "could not bring the context under the limit" warning every
1680
+ * threshold tick. The **cap** calculation subtracts a shape-aware reserve
1681
+ * (`2 × geometry(shape).capacity` chars worth of text edges, billed at the
1682
+ * tiktoken cl100k baseline, plus a 2k summary-template allowance) sized
1683
+ * from the same `shape` snapcompact will use, so the projection still
1684
+ * passes once frames land — but it MUST NOT gate the skip decision, since
1685
+ * a frame-less archive (`text.length <= 2 * edgeCap` short-circuit in
1686
+ * `planArchive`) typically costs only a few hundred tokens of summary
1687
+ * lead and would fit under residual headroom far smaller than the cap
1688
+ * reserve (chatgpt-codex reviews on #3249).
1689
+ *
1690
+ * Returns `1` when the frame charge would overflow but the text-only path
1691
+ * still has room: snapcompact's planner picks the frame-less layout
1692
+ * automatically when the discarded text fits in the edges, so giving it
1693
+ * the minimum cap lets it succeed instead of being skipped outright.
1694
+ *
1695
+ * Without this cap, the bundled `MAX_FRAMES_DEFAULT = 80` × 5024 tokens =
1696
+ * ~402k frame-token projection always overflows any sub-1M-token window
1697
+ * (issue #3247).
1698
+ */
1699
+ #computeSnapcompactMaxFrames(preparation: CompactionPreparation, settings: CompactionSettings): number {
1700
+ const ctxWindow = this.#model?.contextWindow ?? 0;
1701
+ if (ctxWindow <= 0) return Math.min(snapcompact.MAX_FRAMES_DEFAULT, snapcompact.maxFramesForDataBudget());
1702
+ const reserve = effectiveReserveTokens(ctxWindow, settings);
1703
+ let baseTokens = computeNonMessageTokens(this.#host.nonMessageTokenSource());
1704
+ for (const message of preparation.recentMessages) {
1705
+ baseTokens += estimateTokens(message);
1706
+ }
1707
+ const totalBudget = ctxWindow - reserve;
1708
+ // Skip iff there is no headroom whatsoever; a text-only archive costs
1709
+ // far less than the cap reserve below, so any positive residual is
1710
+ // worth attempting and the projection guard catches actual overflow.
1711
+ if (baseTokens >= totalBudget) return 0;
1712
+ // Cap reserve mirrors what `estimateTokens(summaryMessage)` will charge
1713
+ // when frames > 0: `countTokens(summaryTemplate ‖ textHead ‖ textTail)`
1714
+ // plus `numFrames × FRAME_TOKEN_ESTIMATE`. Resolve the shape this
1715
+ // snapcompact pass will actually use (matches the `shape` argument
1716
+ // passed to `snapcompact.compact` in the auto and manual paths) so the
1717
+ // text-edge cost reflects the live frame geometry rather than a fixed
1718
+ // approximation. Reviewer (chatgpt-codex on #3249): a 4k reserve
1719
+ // undersized the ~7k text-edge cost on the default Anthropic
1720
+ // 11on16-bw shape, so the projection then rejected the `maxFrames`
1721
+ // the cap had picked and the warning loop reappeared.
1722
+ //
1723
+ // - `textHead` and `textTail` each consume up to `geometry.capacity`
1724
+ // chars when frames > 0 (one HQ-capacity page per edge: see
1725
+ // `TEXT_EDGE_PAGES = 1` in `planArchive`), so 2 × capacity chars
1726
+ // total. Per-shape capacity: Anthropic 11on16-bw ~13.9k, Opus
1727
+ // 1932px ~21k, Gemini 8on22-bw 2048px ~23.8k, OpenAI 1568px ~13.9k.
1728
+ // - tiktoken cl100k ≈ 4 chars/token on ASCII (verified empirically
1729
+ // for prose, code, and JSON); a 1.15 multiplier absorbs tokenizer
1730
+ // drift on denser content (e.g. dense JSON / tool-result blobs).
1731
+ // - Summary template (intro + FILES section + grid notes) bills
1732
+ // ~2k tokens for typical sessions.
1733
+ const shape = snapcompact.resolveShape(this.#model, this.#host.settings.get("snapcompact.shape"));
1734
+ const edgeCap = snapcompact.geometry(shape).capacity;
1735
+ const textEdgeTokens = Math.ceil((2 * edgeCap * 1.15) / 4);
1736
+ const SUMMARY_TEMPLATE_TOKENS = 2000;
1737
+ const capReserve = textEdgeTokens + SUMMARY_TEMPLATE_TOKENS;
1738
+ const frameBudget = totalBudget - baseTokens - capReserve;
1739
+ if (frameBudget < snapcompact.FRAME_TOKEN_ESTIMATE) return 1;
1740
+ return Math.min(
1741
+ Math.floor(frameBudget / snapcompact.FRAME_TOKEN_ESTIMATE),
1742
+ snapcompact.MAX_FRAMES_DEFAULT,
1743
+ snapcompact.maxFramesForDataBudget(),
1744
+ );
1745
+ }
1746
+
1747
+ #snapcompactFramePayloadBytes(result: snapcompact.CompactionResult): number {
1748
+ const archive = snapcompact.getPreservedArchive(result.preserveData);
1749
+ return archive ? snapcompact.frameDataBytes(archive.frames) : 0;
1750
+ }
1751
+
1752
+ /**
1753
+ * Project the post-compaction context size of a snapcompact result: kept
1754
+ * recent messages + the summary message with its re-attached frames + the
1755
+ * fixed non-message overhead (system prompt + tools). Mirrors how the
1756
+ * compacted context is rebuilt, so the estimate matches the wire shape, and
1757
+ * lets the caller decide whether snapcompact brought the context under the
1758
+ * window or should fall back to an LLM summary.
1759
+ */
1760
+ #projectSnapcompactContextTokens(preparation: CompactionPreparation, result: snapcompact.CompactionResult): number {
1761
+ const archive = snapcompact.getPreservedArchive(result.preserveData);
1762
+ const blocks = archive
1763
+ ? snapcompact.historyBlocks(archive, { maxFrameDataBytes: snapcompact.FRAME_DATA_BYTES_BUDGET })
1764
+ : undefined;
1765
+ const summaryMessage = createCompactionSummaryMessage(
1766
+ result.summary,
1767
+ result.tokensBefore,
1768
+ new Date().toISOString(),
1769
+ result.shortSummary,
1770
+ undefined,
1771
+ undefined,
1772
+ blocks,
1773
+ );
1774
+ let tokens = computeNonMessageTokens(this.#host.nonMessageTokenSource()) + estimateTokens(summaryMessage);
1775
+ for (const message of preparation.recentMessages) {
1776
+ tokens += estimateTokens(message);
1777
+ }
1778
+ return tokens;
1779
+ }
1780
+
1781
+ /**
1782
+ * Post-maintenance progress check for the context-full / snapcompact tail.
1783
+ *
1784
+ * After `appendCompaction` rewrote history and `replaceMessages` swapped in the
1785
+ * compacted context, measure the residual context off the live message set and
1786
+ * decide whether maintenance actually created headroom. Mirrors the shake
1787
+ * recovery-band logic (#2275): a session whose single most-recent turn already
1788
+ * blows the threshold cannot be reduced by compaction (findCutPoint keeps that
1789
+ * turn verbatim), so re-firing on the next agent_end just thrashes. We only
1790
+ * report progress when residual context lands at or below
1791
+ * `COMPACTION_RECOVERY_BAND × threshold` — a band that sits strictly under the
1792
+ * compaction threshold, so reaching it guarantees the next turn cannot
1793
+ * re-trip threshold compaction.
1794
+ *
1795
+ * When the model/window is unknown we cannot evaluate the band, so we
1796
+ * optimistically allow the continuation (preserving prior behavior).
1797
+ */
1798
+ #compactionCreatedHeadroom(): boolean {
1799
+ const contextWindow = this.#model?.contextWindow ?? 0;
1800
+ if (contextWindow <= 0) return true;
1801
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1802
+ const residualTokens = compactionContextTokens(
1803
+ this.#host.getContextUsage({ contextWindow })?.tokens ?? 0,
1804
+ this.#estimateStoredContextTokens(),
1805
+ );
1806
+ const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
1807
+ const recoveryBand = Math.floor(thresholdTokens * COMPACTION_RECOVERY_BAND);
1808
+ // Residual at/below the band is authoritative headroom: the band sits
1809
+ // strictly under the compaction threshold, so the next turn cannot
1810
+ // re-trip threshold compaction regardless of how little this pass shaved.
1811
+ // Don't add a secondary "smaller than the trigger" guard — when stale/
1812
+ // tool-output pruning already dropped context under the band before this
1813
+ // pass, the trigger is itself sub-band, and requiring a strict reduction
1814
+ // would suppress a valid continuation and emit a false no-progress warning
1815
+ // even though compaction left the session safe.
1816
+ return residualTokens <= recoveryBand;
1817
+ }
1818
+
1819
+ /**
1820
+ * Retry-side counterpart to {@link #compactionCreatedHeadroom}. An
1821
+ * overflow/incomplete recovery only needs the rebuilt prompt to *fit* the
1822
+ * window again — it does not have to land under the compaction threshold, let
1823
+ * alone the stricter `COMPACTION_RECOVERY_BAND × threshold` hysteresis the
1824
+ * auto-continue thrash guard uses. Reusing the band here turned recoverable
1825
+ * overflows into manual dead-ends: a 200k-window prompt compacted from
1826
+ * overflow down to ~150k is comfortably retryable, but sits above
1827
+ * `0.8 × 170k = 136k` and was wrongly refused (PR #3412 review).
1828
+ *
1829
+ * Measures residual context against the usable budget (`contextWindow - reserve`).
1830
+ * The default absolute reserve can exceed bundled small-context windows, or
1831
+ * nearly consume a 16k-class window; those known-impossible defaults fall
1832
+ * back to the proportional 15% reserve. Explicit valid reserves still define
1833
+ * the usable prompt budget so retries do not enter headroom the user
1834
+ * intentionally reserved. Callers MUST
1835
+ * invoke this AFTER dropping the failed assistant from `this.#host.messages()`, so
1836
+ * the just-failed turn (which the retry prompt will not include) is excluded
1837
+ * from the estimate.
1838
+ *
1839
+ * When the model/window is unknown we cannot evaluate the budget, so we
1840
+ * optimistically allow the retry (preserving prior behavior).
1841
+ */
1842
+ #compactionCreatedRetryFit(): boolean {
1843
+ const contextWindow = this.#model?.contextWindow ?? 0;
1844
+ if (contextWindow <= 0) return true;
1845
+ const compactionSettings = this.#host.settings.getGroup("compaction");
1846
+ const residualTokens = compactionContextTokens(
1847
+ this.#host.getContextUsage({ contextWindow })?.tokens ?? 0,
1848
+ this.#estimateStoredContextTokens(),
1849
+ );
1850
+ const fitBudget = Math.max(0, contextWindow - resolveBudgetReserveTokens(contextWindow, compactionSettings));
1851
+ return residualTokens <= fitBudget;
1852
+ }
1853
+
1854
+ /**
1855
+ * Last-resort tiered reducer when {@link runAutoCompaction} would otherwise
1856
+ * dead-end. The summarizer cut at the only available turn boundary, but the
1857
+ * kept tail is still over the recovery band because a single recent turn (a
1858
+ * large tool-result, a heavy fenced/XML block, attached images) is itself
1859
+ * bigger than the band and `findCutPoint` cannot cut inside one message.
1860
+ *
1861
+ * Tier 1 — `shake("elide")` reaches INSIDE that tail: heavy tool-result /
1862
+ * block content is offloaded to one `artifact://` blob behind a recoverable
1863
+ * placeholder. Skipped when this pass already ran a shake (`skipElide`).
1864
+ * Tier 2 — `dropImages()`: the manual `/shake images` remedy, automated.
1865
+ * Image blocks are stripped from the branch; unlike elided text they are NOT
1866
+ * artifact-recoverable, so this tier only runs once elide has failed the
1867
+ * progress re-test.
1868
+ *
1869
+ * Each tier that rewrote history re-anchors the in-flight context snapshot,
1870
+ * then the caller's progress predicate is re-tested; the first tier that
1871
+ * restores progress emits one info notice describing everything freed and
1872
+ * stops. Returns whether progress was restored — `false` falls through to
1873
+ * the dead-end warning.
1874
+ */
1875
+ async #rescueCompactionDeadEnd(
1876
+ signal: AbortSignal,
1877
+ options: { skipElide: boolean; hasProgress: () => boolean },
1878
+ ): Promise<boolean> {
1879
+ if (signal.aborted) return false;
1880
+ // Tier 0 — a snapcompact pass whose just-written frame archive is itself
1881
+ // the over-budget cost (each pass re-renders the carried-forward text
1882
+ // into MORE frames, so the archive grows past the recovery band and the
1883
+ // elide/image tiers below can never shrink it): rebuild the archive at
1884
+ // a threshold-derived frame budget.
1885
+ const frameRescue = await this.#rescueSnapcompactFrameOverflow(
1886
+ this.#host.sessionManager.getBranch(),
1887
+ this.#host.settings.getGroup("compaction"),
1888
+ signal,
1889
+ );
1890
+ if (frameRescue !== undefined && options.hasProgress()) return true;
1891
+ let elided = 0;
1892
+ let elidedTokens = 0;
1893
+ let elideSink = "placeholders";
1894
+ if (!options.skipElide) {
1895
+ try {
1896
+ const result = await this.#host.shake("elide", { config: RESCUE_SHAKE_CONFIG, signal });
1897
+ elided = result.toolResultsDropped + result.blocksDropped;
1898
+ elidedTokens = result.tokensFreed;
1899
+ if (result.artifactId) elideSink = "an artifact";
1900
+ if (elided > 0) {
1901
+ // The elide pass rewrote history; re-anchor the in-flight snapshot
1902
+ // so the caller's headroom/retry-fit re-test measures the shaken
1903
+ // context.
1904
+ this.#host.rebaseAfterCompaction();
1905
+ }
1906
+ } catch (error) {
1907
+ logger.warn("Dead-end shake rescue failed", {
1908
+ error: error instanceof Error ? error.message : String(error),
1909
+ });
1910
+ }
1911
+ if (elided > 0 && options.hasProgress()) {
1912
+ this.#host.emitNotice(
1913
+ "info",
1914
+ `Compaction dead-end recovery: ${this.#describeElideRescue(elided, elidedTokens, elideSink)} so maintenance could make progress.`,
1915
+ "compaction",
1916
+ );
1917
+ return true;
1918
+ }
1919
+ }
1920
+ if (signal.aborted) return false;
1921
+ let imagesDropped = 0;
1922
+ try {
1923
+ imagesDropped = (await this.#host.dropImages()).removed;
1924
+ if (imagesDropped > 0) this.#host.rebaseAfterCompaction();
1925
+ } catch (error) {
1926
+ logger.warn("Dead-end image-drop rescue failed", {
1927
+ error: error instanceof Error ? error.message : String(error),
1928
+ });
1929
+ }
1930
+ if (imagesDropped > 0 && options.hasProgress()) {
1931
+ const elidedPart = elided > 0 ? `${this.#describeElideRescue(elided, elidedTokens, elideSink)} and ` : "";
1932
+ this.#host.emitNotice(
1933
+ "info",
1934
+ `Compaction dead-end recovery: ${elidedPart}dropped ${imagesDropped} attached image${imagesDropped === 1 ? "" : "s"} so maintenance could make progress.`,
1935
+ "compaction",
1936
+ );
1937
+ return true;
1938
+ }
1939
+ return false;
1940
+ }
1941
+
1942
+ /** Notice fragment for a dead-end elide tier: what was freed and where it went. */
1943
+ #describeElideRescue(elided: number, tokensFreed: number, sink: string): string {
1944
+ return `elided ${elided} heavy block${elided === 1 ? "" : "s"} (~${tokensFreed.toLocaleString()} tokens) to ${sink}`;
1945
+ }
1946
+
1947
+ /**
1948
+ * Frame budget for {@link #rescueSnapcompactFrameOverflow}: targets
1949
+ * `COMPACTION_RECOVERY_BAND × threshold` (the same band
1950
+ * {@link #compactionCreatedHeadroom} re-tests), not the window-fit budget
1951
+ * {@link #computeSnapcompactMaxFrames} sizes against — a rebuilt archive
1952
+ * must land back under the maintenance trigger, or the next settle
1953
+ * re-enters the same dead-end. Cap reserve mirrors
1954
+ * #computeSnapcompactMaxFrames (text edges + summary template), and
1955
+ * `keptTailTokens` charges the kept entries AFTER the archive so the
1956
+ * budget mirrors what #compactionCreatedHeadroom will actually measure.
1957
+ * Returns 0 when not even one frame fits that budget — the rebuild could
1958
+ * never create headroom, so the caller must not append it.
1959
+ */
1960
+ #computeSnapcompactRescueMaxFrames(settings: CompactionSettings, keptTailTokens: number): number {
1961
+ const ctxWindow = this.#model?.contextWindow ?? 0;
1962
+ if (ctxWindow <= 0) return Math.min(snapcompact.MAX_FRAMES_DEFAULT, snapcompact.maxFramesForDataBudget());
1963
+ const thresholdTokens = resolveThresholdTokens(ctxWindow, settings);
1964
+ const recoveryBandTokens = Math.floor(thresholdTokens * COMPACTION_RECOVERY_BAND);
1965
+ const baseTokens = computeNonMessageTokens(this.#host.nonMessageTokenSource());
1966
+ const shape = snapcompact.resolveShape(this.#model, this.#host.settings.get("snapcompact.shape"));
1967
+ const edgeCap = snapcompact.geometry(shape).capacity;
1968
+ const textEdgeTokens = Math.ceil((2 * edgeCap * 1.15) / 4);
1969
+ const SUMMARY_TEMPLATE_TOKENS = 2000;
1970
+ const frameBudget = recoveryBandTokens - baseTokens - keptTailTokens - textEdgeTokens - SUMMARY_TEMPLATE_TOKENS;
1971
+ if (frameBudget < snapcompact.FRAME_TOKEN_ESTIMATE) return 0;
1972
+ // Same hard caps as #computeSnapcompactMaxFrames: a threshold-derived
1973
+ // count above the per-request payload budget would "shrink" a huge
1974
+ // archive to a frame count the rebuilt prompt can never attach anyway.
1975
+ return Math.min(
1976
+ Math.floor(frameBudget / snapcompact.FRAME_TOKEN_ESTIMATE),
1977
+ snapcompact.MAX_FRAMES_DEFAULT,
1978
+ snapcompact.maxFramesForDataBudget(),
1979
+ );
1980
+ }
1981
+
1982
+ /**
1983
+ * Dead-end rescue for a branch whose latest snapcompact CompactionEntry is
1984
+ * itself billed past the maintenance threshold
1985
+ * (`FRAME_TOKEN_ESTIMATE × frames`). Reaching the `!preparation` dead-end
1986
+ * proves everything after that entry is already kept-recent (nothing to
1987
+ * summarize), so the archive is the irreducible cost — and the elide/image
1988
+ * tiers can never touch it: `collectShakeRegions` and `dropImages()` only
1989
+ * inspect "message"/"custom_message" entries, so a `type: "compaction"`
1990
+ * entry falls through both and the session re-warns on every resume (the
1991
+ * shape issue #4786's rescue does not cover).
1992
+ *
1993
+ * Rebuilds the SAME archive locally — no LLM, no network — by re-running
1994
+ * `snapcompact.compact()` over the entry's carried-forward source text at
1995
+ * a maxFrames derived from the trigger threshold instead of the window:
1996
+ * `planArchive` truncates the oldest chars to fit, so the rebuilt entry
1997
+ * genuinely shrinks. The rebuilt entry keeps the stale entry's
1998
+ * `firstKeptEntryId`, so the kept tail is untouched, and persisting
1999
+ * through `appendCompaction()` lets the write-time superseded-compaction
2000
+ * elision drop the stale frame payload from the JSONL automatically.
2001
+ */
2002
+ async #rescueSnapcompactFrameOverflow(
2003
+ branchEntries: SessionEntry[],
2004
+ settings: CompactionSettings,
2005
+ signal: AbortSignal,
2006
+ ): Promise<snapcompact.CompactionResult | undefined> {
2007
+ if (signal.aborted) return undefined;
2008
+ // Re-rendering frames needs a vision-capable model, same gate as the
2009
+ // snapcompact strategy path.
2010
+ if (!this.#model?.input.includes("image")) return undefined;
2011
+ const staleEntry = getLatestCompactionEntry(branchEntries);
2012
+ if (!staleEntry) return undefined;
2013
+ // Only rescue when the archive is the actual source of the overflow.
2014
+ // The frame budget below charges every kept entry the rebuilt context
2015
+ // will still carry — the kept-recent region from `firstKeptEntryId`
2016
+ // (re-emitted before the archive by buildSessionContext) plus the
2017
+ // entries after the archive — on top of the fixed context, mirroring
2018
+ // what #compactionCreatedHeadroom will measure. When not even one
2019
+ // frame fits (e.g. a huge kept tool result dominates), rebuilding
2020
+ // would append the replacement compaction at the leaf — turning the
2021
+ // branch tail into a compaction entry, which prepareCompaction's
2022
+ // last-entry guard can never summarize past even after an elide
2023
+ // shrinks the real culprit. Bail and let the elide/image tiers handle
2024
+ // that tail instead.
2025
+ let keptTailTokens = 0;
2026
+ let inKeptRegion = false;
2027
+ for (const entry of branchEntries) {
2028
+ if (entry.id === staleEntry.firstKeptEntryId) inKeptRegion = true;
2029
+ if (entry.id === staleEntry.id) {
2030
+ // Everything after the archive is always kept.
2031
+ inKeptRegion = true;
2032
+ continue;
2033
+ }
2034
+ if (!inKeptRegion) continue;
2035
+ const message = (entry as { message?: AgentMessage }).message;
2036
+ if (message) keptTailTokens += estimateTokens(message);
2037
+ }
2038
+ const archive = snapcompact.getPreservedArchive(staleEntry.preserveData);
2039
+ if (!archive || archive.frames.length <= 1) return undefined;
2040
+ const archiveText = snapcompact.archiveSourceText(archive);
2041
+ if (!archiveText) return undefined;
2042
+ const maxFrames = this.#computeSnapcompactRescueMaxFrames(settings, keptTailTokens);
2043
+ if (maxFrames < 1 || maxFrames >= archive.frames.length) return undefined;
2044
+
2045
+ const staleDetails = staleEntry.details as snapcompact.CompactionDetails | undefined;
2046
+ const fileOps = snapcompact.createFileOps();
2047
+ for (const file of staleDetails?.readFiles ?? []) fileOps.read.add(file);
2048
+ for (const file of staleDetails?.modifiedFiles ?? []) fileOps.edited.add(file);
2049
+ const shapeSetting = this.#host.settings.get("snapcompact.shape");
2050
+ const shape = snapcompact.resolveShapeForText(archiveText, this.#model, shapeSetting);
2051
+ let result: snapcompact.CompactionResult;
2052
+ try {
2053
+ result = await snapcompact.compact(
2054
+ {
2055
+ firstKeptEntryId: staleEntry.firstKeptEntryId,
2056
+ messagesToSummarize: [],
2057
+ turnPrefixMessages: [],
2058
+ tokensBefore: staleEntry.tokensBefore,
2059
+ previousSummary: staleEntry.summary,
2060
+ previousPreserveData: staleEntry.preserveData,
2061
+ fileOps,
2062
+ },
2063
+ {
2064
+ convertToLlm,
2065
+ model: this.#model,
2066
+ ...(shapeSetting === "auto" ? {} : { shape }),
2067
+ maxFrames,
2068
+ },
2069
+ );
2070
+ } catch (error) {
2071
+ logger.warn("Dead-end snapcompact frame rescue failed", {
2072
+ error: error instanceof Error ? error.message : String(error),
2073
+ });
2074
+ return undefined;
2075
+ }
2076
+ if (signal.aborted) return undefined;
2077
+ const rebuilt = snapcompact.getPreservedArchive(result.preserveData);
2078
+ if (!rebuilt || rebuilt.frames.length >= archive.frames.length) return undefined;
2079
+
2080
+ const rebuiltEntryId = this.#host.sessionManager.appendCompaction(
2081
+ result.summary,
2082
+ result.shortSummary,
2083
+ result.firstKeptEntryId,
2084
+ result.tokensBefore,
2085
+ result.details,
2086
+ false,
2087
+ result.preserveData,
2088
+ );
2089
+ const sessionContext = this.#host.buildDisplaySessionContext();
2090
+ this.#host.agent.replaceMessages(sessionContext.messages);
2091
+ this.#host.rebaseAfterCompaction();
2092
+ // Same post-rewrite bookkeeping as the regular compaction append: the
2093
+ // rebuilt context no longer carries the transient plan reference (#1246),
2094
+ // and advisor cursors / todo phases were derived from the replaced
2095
+ // history.
2096
+ this.#host.resetPlanReference();
2097
+ this.#host.resetAdvisorRuntimes();
2098
+ this.#host.syncTodoPhasesFromBranch();
2099
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
2100
+ // Extensions must see the entry that is now active, not (only) the one
2101
+ // this rebuild just superseded — mirror the regular append path's hook.
2102
+ const rebuiltEntry = this.#host.sessionManager.getEntries().find(e => e.id === rebuiltEntryId) as
2103
+ | CompactionEntry
2104
+ | undefined;
2105
+ if (this.#host.extensionRunner && rebuiltEntry) {
2106
+ await this.#host.extensionRunner.emit({
2107
+ type: "session_compact",
2108
+ compactionEntry: rebuiltEntry,
2109
+ fromExtension: false,
2110
+ });
2111
+ }
2112
+ this.#host.emitNotice(
2113
+ "info",
2114
+ `Compaction dead-end recovery: rebuilt the trailing snapcompact archive at a smaller frame budget (${archive.frames.length} → ${rebuilt.frames.length} frames) so maintenance could make progress.`,
2115
+ "compaction",
2116
+ );
2117
+ return result;
2118
+ }
2119
+
2120
+ /**
2121
+ * Internal: Run auto-compaction with events.
2122
+ *
2123
+ * @param allowDefer If true (default), threshold-driven handoff strategy is allowed to
2124
+ * schedule itself as a deferred post-prompt task and return a deferred-handoff result
2125
+ * immediately. The caller MUST treat that as "compaction will happen async — do not
2126
+ * also schedule `agent.continue()` for this turn", otherwise the deferred handoff
2127
+ * races a fresh streaming turn (the symptom: "Auto-handoff" loader + assistant
2128
+ * message still streaming). Callers on a path that is about to start a new agent
2129
+ * turn (e.g. the pre-prompt check in `#promptWithMessage`) pass `false` to force
2130
+ * inline execution so the handoff completes before the new turn begins.
2131
+ * @returns whether auto-compaction scheduled a follow-up turn.
2132
+ */
2133
+ async runAutoCompaction(
2134
+ reason: "overflow" | "threshold" | "idle" | "incomplete",
2135
+ willRetry: boolean,
2136
+ deferred = false,
2137
+ allowDefer = true,
2138
+ options: {
2139
+ autoContinue?: boolean;
2140
+ triggerContextTokens?: number;
2141
+ suppressContinuation?: boolean;
2142
+ suppressHandoff?: boolean;
2143
+ phase?: CodexCompactionContext["phase"];
2144
+ terminalTextAnswer?: boolean;
2145
+ } = {},
2146
+ ): Promise<CompactionCheckResult> {
2147
+ const compactionSettings = this.#host.settings.getGroup("compaction");
2148
+ if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
2149
+ if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
2150
+ const generation = this.#host.promptGeneration();
2151
+ const terminalTextAnswer =
2152
+ options.terminalTextAnswer ?? isTerminalTextAssistantAnswer(this.#host.findLastAssistantMessage());
2153
+ const suppressContinuation = options.suppressContinuation === true;
2154
+ const shouldAutoContinue =
2155
+ !suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
2156
+ const suppressHandoff = options.suppressHandoff === true;
2157
+ let fallbackFromShake = false;
2158
+ // Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
2159
+ // reclaims nothing we fall through to the summary-compaction body below so
2160
+ // the oversized input still gets resolved.
2161
+ if (compactionSettings.strategy === "shake") {
2162
+ const outcome = await this.#runAutoShake(
2163
+ reason,
2164
+ willRetry,
2165
+ generation,
2166
+ shouldAutoContinue,
2167
+ terminalTextAnswer,
2168
+ options.triggerContextTokens,
2169
+ suppressContinuation,
2170
+ );
2171
+ if (outcome !== "fallback") return outcome;
2172
+ fallbackFromShake = true;
2173
+ }
2174
+ // "overflow" and "incomplete" force inline execution because they are recovery
2175
+ // paths the caller wants resolved before scheduling the next turn. "idle" is
2176
+ // triggered by the idle loop and does its own scheduling.
2177
+ if (
2178
+ !suppressHandoff &&
2179
+ !deferred &&
2180
+ allowDefer &&
2181
+ reason !== "overflow" &&
2182
+ reason !== "incomplete" &&
2183
+ reason !== "idle" &&
2184
+ compactionSettings.strategy === "handoff"
2185
+ ) {
2186
+ this.#host.schedulePostPromptTask(
2187
+ async signal => {
2188
+ await Promise.resolve();
2189
+ if (signal.aborted) return;
2190
+ await this.runAutoCompaction(reason, willRetry, true, true, {
2191
+ ...options,
2192
+ terminalTextAnswer,
2193
+ });
2194
+ },
2195
+ { generation },
2196
+ );
2197
+ return {
2198
+ ...COMPACTION_CHECK_DEFERRED_HANDOFF,
2199
+ continuationScheduled: shouldAutoContinue,
2200
+ };
2201
+ }
2202
+
2203
+ // "overflow" forces context-full because the input itself is broken — a handoff
2204
+ // LLM call would hit the same overflow. "incomplete" is an output-side problem,
2205
+ // so a handoff request on the existing context is still viable.
2206
+ let action: "context-full" | "handoff" | "snapcompact" =
2207
+ compactionSettings.strategy === "snapcompact"
2208
+ ? "snapcompact"
2209
+ : compactionSettings.strategy === "handoff" && reason !== "overflow" && !suppressHandoff
2210
+ ? "handoff"
2211
+ : "context-full";
2212
+ if (action === "snapcompact" && this.#model && !this.#model.input.includes("image")) {
2213
+ this.#host.emitNotice(
2214
+ "warning",
2215
+ `snapcompact needs a vision-capable active model (${this.#model.id} is text-only); using context-full auto-compaction instead.`,
2216
+ "compaction",
2217
+ );
2218
+ action = "context-full";
2219
+ }
2220
+ // Abort any older auto-compaction before installing this run's controller.
2221
+ this.#autoCompactionAbortController?.abort();
2222
+ const autoCompactionAbortController = new AbortController();
2223
+ this.#autoCompactionAbortController = autoCompactionAbortController;
2224
+ const autoCompactionSignal = autoCompactionAbortController.signal;
2225
+
2226
+ try {
2227
+ // Emit start AFTER the controller is installed so isCompacting is already true
2228
+ // for any listener — and for input routed during this emit's event-loop yield:
2229
+ // a message typed as the compaction loader appears must land in the compaction
2230
+ // queue, not the core steering queue (which handoff's agent.reset() would wipe).
2231
+ await this.#host.emitSessionEvent({ type: "auto_compaction_start", reason, action });
2232
+ if (action === "handoff") {
2233
+ let handoffSwitchCancelled = false;
2234
+ const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
2235
+ const handoffResult = await this.#host.runHandoff(handoffFocus, {
2236
+ autoTriggered: true,
2237
+ signal: autoCompactionSignal,
2238
+ onSwitchCancelled: () => {
2239
+ handoffSwitchCancelled = true;
2240
+ },
2241
+ });
2242
+ if (!handoffResult) {
2243
+ const aborted = autoCompactionSignal.aborted || handoffSwitchCancelled;
2244
+ if (aborted) {
2245
+ await this.#host.emitSessionEvent({
2246
+ type: "auto_compaction_end",
2247
+ action,
2248
+ result: undefined,
2249
+ aborted: true,
2250
+ willRetry: false,
2251
+ });
2252
+ return COMPACTION_CHECK_NONE;
2253
+ }
2254
+ logger.warn("Auto-handoff returned no document; falling back to context-full maintenance", {
2255
+ reason,
2256
+ });
2257
+ action = "context-full";
2258
+ }
2259
+ if (handoffResult) {
2260
+ await this.#host.emitSessionEvent({
2261
+ type: "auto_compaction_end",
2262
+ action,
2263
+ result: undefined,
2264
+ aborted: false,
2265
+ willRetry: false,
2266
+ });
2267
+ const continuationScheduled =
2268
+ !autoCompactionSignal.aborted &&
2269
+ this.#host.scheduleCompactionContinuation({
2270
+ generation,
2271
+ autoContinue: reason !== "idle" && shouldAutoContinue,
2272
+ terminalTextAnswer,
2273
+ suppressContinuation,
2274
+ });
2275
+ return {
2276
+ ...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
2277
+ historyRewritten: true,
2278
+ };
2279
+ }
2280
+ }
2281
+
2282
+ if (!this.#model) {
2283
+ await this.#host.emitSessionEvent({
2284
+ type: "auto_compaction_end",
2285
+ action,
2286
+ result: undefined,
2287
+ aborted: false,
2288
+ willRetry: false,
2289
+ skipped: true,
2290
+ });
2291
+ return COMPACTION_CHECK_NONE;
2292
+ }
2293
+
2294
+ const availableModels = this.#host.modelRegistry.getAvailable();
2295
+ if (availableModels.length === 0) {
2296
+ await this.#host.emitSessionEvent({
2297
+ type: "auto_compaction_end",
2298
+ action,
2299
+ result: undefined,
2300
+ aborted: false,
2301
+ willRetry: false,
2302
+ skipped: true,
2303
+ });
2304
+ return COMPACTION_CHECK_NONE;
2305
+ }
2306
+
2307
+ const pathEntries = this.#host.sessionManager.getBranch();
2308
+
2309
+ let pathEntriesForCompaction = pathEntries;
2310
+ let preparation = prepareCompaction(pathEntriesForCompaction, compactionSettings, this.#model);
2311
+ if (!preparation) {
2312
+ // prepareCompaction found nothing to summarize because the kept region
2313
+ // is a single oversized recent turn — findCutPoint never cuts inside a
2314
+ // tool result, so a huge tool-result / fenced block tail leaves nothing
2315
+ // on the summarizable side and summary compaction cannot even start.
2316
+ // That is exactly the dead-end the elide shake rescues: it reaches
2317
+ // INSIDE the tail and offloads heavy content to an artifact placeholder,
2318
+ // shrinking the tail so findCutPoint can then move the cut and leave
2319
+ // older turns to summarize. Run the same tiered rescue the
2320
+ // post-maintenance guard uses (elide, then image drop), with progress
2321
+ // defined as "prepareCompaction now succeeds on the rewritten branch",
2322
+ // and fall through to the normal compaction body when it does (writing
2323
+ // a compaction entry anchors the stale billed usage so the
2324
+ // auto-continue re-check cannot re-trip and loop the warning — issue
2325
+ // #4786). `skipElide` when we already fell through from a shake
2326
+ // strategy pass (it tried and found nothing); skip entirely on the
2327
+ // idle timer (it re-checks usage on its own cadence).
2328
+ let rescueRewroteHistory = false;
2329
+ // A snapcompact CompactionEntry is invisible to both rescue tiers
2330
+ // below (they only inspect message entries) and to prepareCompaction
2331
+ // itself (last-entry-is-compaction guard), so a frame archive billed
2332
+ // past the threshold dead-ends here on every resume. Rebuild it at a
2333
+ // threshold-derived frame budget first — but treat that as complete
2334
+ // only when it actually created headroom: the latest archive may not
2335
+ // be the oversized tail (e.g. a huge kept tool result after it), and
2336
+ // declaring victory on a mere frame-count shrink would skip the
2337
+ // elide/image tiers that can still reach that tail and suppress a
2338
+ // warning the user should see.
2339
+ let frameRescueResult: snapcompact.CompactionResult | undefined;
2340
+ let frameRescueCreatedHeadroom = false;
2341
+ if (reason !== "idle") {
2342
+ frameRescueResult = await this.#rescueSnapcompactFrameOverflow(
2343
+ pathEntriesForCompaction,
2344
+ compactionSettings,
2345
+ autoCompactionSignal,
2346
+ );
2347
+ if (frameRescueResult) {
2348
+ rescueRewroteHistory = true;
2349
+ pathEntriesForCompaction = this.#host.sessionManager.getBranch();
2350
+ frameRescueCreatedHeadroom = this.#compactionCreatedHeadroom();
2351
+ }
2352
+ if (!frameRescueCreatedHeadroom) {
2353
+ await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
2354
+ skipElide: fallbackFromShake,
2355
+ hasProgress: () => {
2356
+ // Only reached when a tier actually freed something, so the
2357
+ // branch has been rewritten either way.
2358
+ rescueRewroteHistory = true;
2359
+ pathEntriesForCompaction = this.#host.sessionManager.getBranch();
2360
+ preparation = prepareCompaction(pathEntriesForCompaction, compactionSettings, this.#model);
2361
+ return preparation !== undefined;
2362
+ },
2363
+ });
2364
+ }
2365
+ }
2366
+ if (!preparation) {
2367
+ const noProgressDeadEnd = reason !== "idle" && !frameRescueCreatedHeadroom;
2368
+ const deadEndWarning = noProgressDeadEnd
2369
+ ? compactionDeadEndWarning("shrink it (e.g. clear large tool output)")
2370
+ : undefined;
2371
+ // A rescue that appended a rebuilt archive without creating
2372
+ // headroom must carry the dead-end badge on the entry the
2373
+ // transcript actually shows (the rebuilt one), or the pause
2374
+ // loses its explanation once the notice scrolls away. Stamp it
2375
+ // BEFORE the auto_compaction_end event: a result-carrying event
2376
+ // makes the TUI rebuild the chat from the current entries
2377
+ // immediately, so a later stamp would not appear until some
2378
+ // unrelated rebuild.
2379
+ if (deadEndWarning && frameRescueResult) {
2380
+ const stampEntry = getLatestCompactionEntry(this.#host.sessionManager.getBranch());
2381
+ if (stampEntry) {
2382
+ stampEntry.warning = deadEndWarning;
2383
+ await this.#host.sessionManager.rewriteEntries();
2384
+ }
2385
+ }
2386
+ // A successful frame rescue rewrote history and activated a new
2387
+ // compaction entry — surface it as a real (non-skipped) result so
2388
+ // the TUI rebuilds the transcript instead of treating the pass as
2389
+ // a benign no-op.
2390
+ await this.#host.emitSessionEvent({
2391
+ type: "auto_compaction_end",
2392
+ action,
2393
+ result: frameRescueResult,
2394
+ aborted: false,
2395
+ willRetry: false,
2396
+ skipped: frameRescueResult === undefined,
2397
+ });
2398
+ let continuationScheduled = false;
2399
+ if (frameRescueCreatedHeadroom) {
2400
+ continuationScheduled = this.#host.scheduleCompactionContinuation({
2401
+ generation,
2402
+ autoContinue: shouldAutoContinue,
2403
+ terminalTextAnswer,
2404
+ suppressContinuation,
2405
+ });
2406
+ } else if (!suppressContinuation && this.#host.agent.hasQueuedMessages()) {
2407
+ this.#host.scheduleAgentContinue({
2408
+ delayMs: 100,
2409
+ generation,
2410
+ shouldContinue: () => this.#host.agent.hasQueuedMessages(),
2411
+ });
2412
+ continuationScheduled = true;
2413
+ }
2414
+ if (deadEndWarning) {
2415
+ this.#host.emitNotice("warning", deadEndWarning, "compaction");
2416
+ }
2417
+ // A rescue that offloaded content but still could not produce a
2418
+ // preparation rewrote the branch; flag it so the overflow-recovery
2419
+ // rollback does not re-restore the just-failed assistant turn on top
2420
+ // of the elided tail.
2421
+ const base = continuationScheduled
2422
+ ? COMPACTION_CHECK_CONTINUATION
2423
+ : noProgressDeadEnd
2424
+ ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION
2425
+ : COMPACTION_CHECK_NONE;
2426
+ return rescueRewroteHistory ? { ...base, historyRewritten: true } : base;
2427
+ }
2428
+ }
2429
+
2430
+ let hookCompaction: CompactionResult | undefined;
2431
+ let fromExtension = false;
2432
+ let preserveData: Record<string, unknown> | undefined;
2433
+ let codexCompaction: CodexCompactionContext | undefined;
2434
+
2435
+ if (this.#host.extensionRunner?.hasHandlers("session_before_compact")) {
2436
+ const hookResult = (await this.#host.extensionRunner.emit({
2437
+ type: "session_before_compact",
2438
+ preparation,
2439
+ branchEntries: pathEntriesForCompaction,
2440
+ customInstructions: undefined,
2441
+ signal: autoCompactionSignal,
2442
+ })) as SessionBeforeCompactResult | undefined;
2443
+
2444
+ if (hookResult?.cancel) {
2445
+ await this.#host.emitSessionEvent({
2446
+ type: "auto_compaction_end",
2447
+ action,
2448
+ result: undefined,
2449
+ aborted: true,
2450
+ willRetry: false,
2451
+ });
2452
+ return COMPACTION_CHECK_NONE;
2453
+ }
2454
+
2455
+ if (hookResult?.compaction) {
2456
+ hookCompaction = hookResult.compaction;
2457
+ fromExtension = true;
2458
+ }
2459
+ }
2460
+
2461
+ const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
2462
+
2463
+ let summary: string;
2464
+ let shortSummary: string | undefined;
2465
+ let firstKeptEntryId: string;
2466
+ let tokensBefore: number;
2467
+ let details: unknown;
2468
+
2469
+ // Snapcompact runs locally first. The post-compaction context = kept-recent
2470
+ // + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
2471
+ // per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
2472
+ // live window so we don't run snapcompact just to overflow every threshold
2473
+ // tick. Any local blocker (unsupported snapcompact glyphs, kept-history too large,
2474
+ // post-render overflow) downgrades auto maintenance to a context-full LLM
2475
+ // summary instead of wedging the session (#3659) — auto runs the default
2476
+ // strategy on the user's behalf, so a fallback that lets the session keep
2477
+ // running is the right behavior. Manual `/compact snapcompact` keeps the
2478
+ // local-only contract (#3599): the user explicitly picked it.
2479
+ let snapcompactResult: snapcompact.CompactionResult | undefined;
2480
+ let snapcompactBlocker: string | undefined;
2481
+ if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
2482
+ // Drop `¶think:` sections for Anthropic-dialect targets: the archive
2483
+ // is replayed as text and Claude refuses reproduced reasoning
2484
+ // ("reasoning_extraction", issue #6093).
2485
+ const snapcompactIncludeThinking = preferredDialect(this.#model.id) !== "anthropic";
2486
+ const text = snapcompact.serializeConversation(
2487
+ convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
2488
+ { includeThinking: snapcompactIncludeThinking },
2489
+ );
2490
+ const probeText = snapcompact.renderabilityProbeText(
2491
+ text,
2492
+ preparation.previousPreserveData,
2493
+ preparation.previousSummary,
2494
+ );
2495
+ const shapeSetting = this.#host.settings.get("snapcompact.shape");
2496
+ const shape = snapcompact.resolveShapeForText(probeText, this.#model, shapeSetting);
2497
+ const renderScan = snapcompact.scanRenderability(probeText, { shape });
2498
+ if (!renderScan.isSafe) {
2499
+ const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
2500
+ logger.warn("Snapcompact disabled: unsupported characters for selected snapcompact font", {
2501
+ model: this.#model?.id,
2502
+ unrenderableRatio: renderScan.unrenderableRatio,
2503
+ });
2504
+ snapcompactBlocker = `snapcompact disabled: unsupported characters for selected snapcompact font (${percent}%); using context-full auto-compaction instead.`;
2505
+ } else {
2506
+ const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
2507
+ if (maxFrames < 1) {
2508
+ logger.warn("Snapcompact skipped: kept history alone exceeds the context budget", {
2509
+ model: this.#model?.id,
2510
+ });
2511
+ snapcompactBlocker =
2512
+ "snapcompact: kept history alone exceeds the context budget; using context-full auto-compaction instead.";
2513
+ } else {
2514
+ snapcompactResult = await snapcompact.compact(preparation, {
2515
+ convertToLlm,
2516
+ model: this.#model,
2517
+ ...(shapeSetting === "auto" ? {} : { shape }),
2518
+ maxFrames,
2519
+ includeThinking: snapcompactIncludeThinking,
2520
+ });
2521
+ const framePayloadBytes = this.#snapcompactFramePayloadBytes(snapcompactResult);
2522
+ if (framePayloadBytes > snapcompact.FRAME_DATA_BYTES_BUDGET) {
2523
+ logger.warn("Snapcompact exceeded the per-request frame payload budget", {
2524
+ model: this.#model?.id,
2525
+ framePayloadBytes,
2526
+ budget: snapcompact.FRAME_DATA_BYTES_BUDGET,
2527
+ });
2528
+ snapcompactBlocker =
2529
+ "snapcompact produced too much standing image payload; using context-full auto-compaction instead.";
2530
+ snapcompactResult = undefined;
2531
+ }
2532
+ if (snapcompactResult) {
2533
+ const ctxWindow = this.#model?.contextWindow ?? 0;
2534
+ const budget =
2535
+ ctxWindow > 0
2536
+ ? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
2537
+ : Number.POSITIVE_INFINITY;
2538
+ const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
2539
+ if (projected > budget) {
2540
+ logger.warn("Snapcompact still overflows the window after frame-budget sizing", {
2541
+ model: this.#model?.id,
2542
+ projected,
2543
+ budget,
2544
+ });
2545
+ snapcompactBlocker =
2546
+ "snapcompact could not bring the context under the limit; using context-full auto-compaction instead.";
2547
+ snapcompactResult = undefined;
2548
+ }
2549
+ }
2550
+ }
2551
+ }
2552
+ if (snapcompactBlocker) {
2553
+ this.#host.emitNotice("warning", snapcompactBlocker, "compaction");
2554
+ action = "context-full";
2555
+ }
2556
+ }
2557
+
2558
+ if (compactionPrep.kind === "fromHook") {
2559
+ summary = compactionPrep.summary;
2560
+ shortSummary = compactionPrep.shortSummary;
2561
+ firstKeptEntryId = compactionPrep.firstKeptEntryId;
2562
+ tokensBefore = compactionPrep.tokensBefore;
2563
+ details = compactionPrep.details;
2564
+ preserveData = compactionPrep.preserveData;
2565
+ } else if (snapcompactResult) {
2566
+ summary = snapcompactResult.summary;
2567
+ shortSummary = snapcompactResult.shortSummary;
2568
+ firstKeptEntryId = snapcompactResult.firstKeptEntryId;
2569
+ tokensBefore = snapcompactResult.tokensBefore;
2570
+ details = snapcompactResult.details;
2571
+ preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
2572
+ } else {
2573
+ const candidates = this.#getCompactionModelCandidates(availableModels);
2574
+ const retrySettings = this.#host.settings.getGroup("retry");
2575
+ const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
2576
+ let compactResult: CompactionResult | undefined;
2577
+ let lastError: unknown;
2578
+ let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
2579
+ codexCompaction = createCodexCompactionContext({
2580
+ trigger: "auto",
2581
+ reason: "context_limit",
2582
+ phase:
2583
+ options.phase ??
2584
+ (reason === "threshold" ? "pre_turn" : reason === "idle" ? "standalone_turn" : "mid_turn"),
2585
+ });
2586
+
2587
+ for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
2588
+ const candidate = candidates[candidateIndex];
2589
+ const hasMoreCandidates = candidateIndex < candidates.length - 1;
2590
+ const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
2591
+ if (!apiKey) continue;
2592
+ if (
2593
+ nativeCompactionFailure &&
2594
+ (candidate.provider !== nativeCompactionFailure.provider ||
2595
+ !shouldUseProviderNativeCompaction(candidate, preparation.settings))
2596
+ ) {
2597
+ throw nativeCompactionFailure.error;
2598
+ }
2599
+
2600
+ let attempt = 0;
2601
+ while (true) {
2602
+ try {
2603
+ compactResult = await compact(
2604
+ this.#host.obfuscatePreparationForProvider(preparation),
2605
+ candidate,
2606
+ this.#host.modelRegistry.resolver(candidate, this.#host.sessionId()),
2607
+ undefined,
2608
+ autoCompactionSignal,
2609
+ {
2610
+ promptOverride: this.#host.obfuscateTextForProvider(compactionPrep.hookPrompt),
2611
+ extraContext: compactionPrep.hookContext,
2612
+ remoteInstructions: this.#host.baseSystemPrompt().join("\n\n"),
2613
+ metadata: this.#host.agent.metadataForProvider(candidate.provider),
2614
+ initiatorOverride: "agent",
2615
+ convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
2616
+ telemetry,
2617
+ // Honor the user's /model thinking selection on the
2618
+ // auto-compaction path — the most-fired compaction
2619
+ // site. Clamped per-model inside compact() via
2620
+ // resolveCompactionEffort.
2621
+ thinkingLevel: this.#host.thinkingLevel(),
2622
+ tools: this.#host.agent.state.tools,
2623
+ sessionId: this.#host.sessionId(),
2624
+ promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
2625
+ providerSessionState: this.#host.providerSessionState,
2626
+ preferWebsockets: this.#host.preferWebsockets,
2627
+ codexCompaction,
2628
+ },
2629
+ );
2630
+ break;
2631
+ } catch (error) {
2632
+ if (autoCompactionSignal.aborted) {
2633
+ throw error;
2634
+ }
2635
+
2636
+ const message = error instanceof Error ? error.message : String(error);
2637
+ const id = AIError.classify(
2638
+ error instanceof NativeCompactionError ? error.cause : error,
2639
+ candidate.api,
2640
+ );
2641
+ if (AIError.is(id, AIError.Flag.AuthFailed)) {
2642
+ if (!nativeCompactionFailure) lastError = this.#buildCompactionAuthError();
2643
+ break;
2644
+ }
2645
+ if (AIError.is(id, AIError.Flag.Timeout)) {
2646
+ const nativeFailure = error instanceof NativeCompactionError;
2647
+ logger.warn(
2648
+ nativeFailure
2649
+ ? "Provider-native auto-compaction timed out, preserving native failure"
2650
+ : hasMoreCandidates
2651
+ ? "Auto-compaction summarization timed out, trying next model"
2652
+ : "Auto-compaction summarization timed out, not retrying same model",
2653
+ {
2654
+ error: message,
2655
+ model: `${candidate.provider}/${candidate.id}`,
2656
+ },
2657
+ );
2658
+ if (nativeFailure) {
2659
+ nativeCompactionFailure ??= { error, provider: candidate.provider };
2660
+ lastError = nativeCompactionFailure.error;
2661
+ } else {
2662
+ lastError = error;
2663
+ }
2664
+ break;
2665
+ }
2666
+
2667
+ const retryAfterMs = this.#host.parseRetryAfterMsFromError(message);
2668
+ const shouldRetry =
2669
+ retrySettings.enabled &&
2670
+ attempt < retrySettings.maxRetries &&
2671
+ (retryAfterMs !== undefined ||
2672
+ AIError.is(id, AIError.Flag.Transient) ||
2673
+ AIError.is(id, AIError.Flag.UsageLimit));
2674
+ if (!shouldRetry) {
2675
+ if (error instanceof NativeCompactionError) {
2676
+ nativeCompactionFailure ??= { error, provider: candidate.provider };
2677
+ lastError = nativeCompactionFailure.error;
2678
+ } else {
2679
+ lastError = error;
2680
+ }
2681
+ break;
2682
+ }
2683
+
2684
+ const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
2685
+ const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
2686
+
2687
+ // If retry delay is too long (>30s), try next candidate instead of waiting
2688
+ const maxAcceptableDelayMs = 30_000;
2689
+ if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) {
2690
+ if (error instanceof NativeCompactionError) {
2691
+ nativeCompactionFailure ??= { error, provider: candidate.provider };
2692
+ lastError = nativeCompactionFailure.error;
2693
+ break;
2694
+ }
2695
+ logger.warn("Auto-compaction retry delay too long, trying next model", {
2696
+ delayMs,
2697
+ retryAfterMs,
2698
+ error: message,
2699
+ model: `${candidate.provider}/${candidate.id}`,
2700
+ });
2701
+ lastError = error;
2702
+ break; // Exit retry loop, continue to next candidate
2703
+ }
2704
+
2705
+ attempt++;
2706
+ logger.warn("Auto-compaction failed, retrying", {
2707
+ attempt,
2708
+ maxRetries: retrySettings.maxRetries,
2709
+ delayMs,
2710
+ retryAfterMs,
2711
+ error: message,
2712
+ model: `${candidate.provider}/${candidate.id}`,
2713
+ });
2714
+ await scheduler.wait(delayMs, { signal: autoCompactionSignal });
2715
+ }
2716
+ }
2717
+
2718
+ if (compactResult) {
2719
+ break;
2720
+ }
2721
+ }
2722
+
2723
+ if (!compactResult) {
2724
+ if (lastError) {
2725
+ throw lastError;
2726
+ }
2727
+ throw new Error("Compaction failed: no available model");
2728
+ }
2729
+
2730
+ summary = compactResult.summary;
2731
+ shortSummary = compactResult.shortSummary;
2732
+ firstKeptEntryId = compactResult.firstKeptEntryId;
2733
+ tokensBefore = compactResult.tokensBefore;
2734
+ details = compactResult.details;
2735
+ preserveData = mergeLlmCompactionPreserveData(compactionPrep.preserveData, compactResult.preserveData);
2736
+ }
2737
+
2738
+ if (autoCompactionSignal.aborted) {
2739
+ await this.#host.emitSessionEvent({
2740
+ type: "auto_compaction_end",
2741
+ action,
2742
+ result: undefined,
2743
+ aborted: true,
2744
+ willRetry: false,
2745
+ });
2746
+ return COMPACTION_CHECK_NONE;
2747
+ }
2748
+
2749
+ this.#host.sessionManager.appendCompaction(
2750
+ summary,
2751
+ shortSummary,
2752
+ firstKeptEntryId,
2753
+ tokensBefore,
2754
+ details,
2755
+ fromExtension,
2756
+ preserveData,
2757
+ );
2758
+ const newEntries = this.#host.sessionManager.getEntries();
2759
+ const sessionContext = this.#host.buildDisplaySessionContext();
2760
+ this.#host.agent.replaceMessages(sessionContext.messages);
2761
+ this.#host.rebaseAfterCompaction();
2762
+ // Compaction discarded the conversation history that carried the approved
2763
+ // plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
2764
+ // the plan from disk and re-injects it on the next turn (issue #1246).
2765
+ this.#host.resetPlanReference();
2766
+ this.#host.resetAdvisorRuntimes();
2767
+ this.#host.syncTodoPhasesFromBranch();
2768
+ if (codexCompaction) {
2769
+ this.#host.resetCodexProviderAfterCompaction(codexCompaction);
2770
+ } else {
2771
+ this.#host.closeCodexProviderSessionsForHistoryRewrite();
2772
+ }
2773
+
2774
+ // Get the saved compaction entry for the hook
2775
+ const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
2776
+ | CompactionEntry
2777
+ | undefined;
2778
+
2779
+ if (this.#host.extensionRunner && savedCompactionEntry) {
2780
+ await this.#host.extensionRunner.emit({
2781
+ type: "session_compact",
2782
+ compactionEntry: savedCompactionEntry,
2783
+ fromExtension,
2784
+ });
2785
+ }
2786
+
2787
+ const result: CompactionResult = {
2788
+ summary,
2789
+ shortSummary,
2790
+ firstKeptEntryId,
2791
+ tokensBefore,
2792
+ details,
2793
+ preserveData,
2794
+ };
2795
+ // Post-maintenance progress guard — evaluated BEFORE emitting
2796
+ // auto_compaction_end so the TUI rebuild triggered by that event
2797
+ // already reflects any rescue rewrite (elide / image-drop) and the
2798
+ // dead-end warning stamped on the compaction entry. Snapcompact can
2799
+ // project over budget and fall back to a context-full summary; the
2800
+ // summarizer keeps `keepRecentTokens` of recent history verbatim and
2801
+ // findCutPoint can only cut at turn boundaries (never tool results),
2802
+ // so a single oversized recent turn (e.g. a huge tool result) leaves
2803
+ // the rewritten context still above threshold. Scheduling the
2804
+ // continuation regardless means the next agent_end re-enters
2805
+ // checkCompaction over the same oversized tail and re-fires forever.
2806
+ // The retry and the threshold auto-continue use different progress
2807
+ // tests (a recoverable overflow only has to fit; the auto-continue
2808
+ // thrash needs the stricter recovery band), so each branch evaluates
2809
+ // its own below.
2810
+ let continuationScheduled = false;
2811
+ // A non-idle pass that wanted to continue (retry or auto-continue) but freed
2812
+ // too little for that path to proceed is a dead-end: warn once so the user
2813
+ // understands why maintenance paused instead of silently looping.
2814
+ let noProgressDeadEnd = false;
2815
+ let retryFits = false;
2816
+ let hasHeadroom = false;
2817
+
2818
+ if (willRetry) {
2819
+ const messages = this.#host.agent.state.messages;
2820
+ const lastMsg = messages[messages.length - 1];
2821
+ if (lastMsg?.role === "assistant") {
2822
+ const lastAssistant = lastMsg as AssistantMessage;
2823
+ // Drop the prior turn before retry when it carries no actionable deliverable:
2824
+ // - "error": failure was kept in history but must not re-enter the next turn's prompt.
2825
+ // - reason === "incomplete" && stopReason === "length": truncated output (typically
2826
+ // reasoning-only) — re-running it produces the same dead-end.
2827
+ const shouldDrop =
2828
+ lastAssistant.stopReason === "error" ||
2829
+ (reason === "incomplete" && lastAssistant.stopReason === "length");
2830
+ if (shouldDrop) {
2831
+ this.#host.agent.replaceMessages(messages.slice(0, -1));
2832
+ this.#host.rebaseAfterCompaction();
2833
+ }
2834
+ }
2835
+
2836
+ // Retry only needs the rebuilt prompt to fit the window again — measured
2837
+ // AFTER the drop above so the just-failed turn (which the retry prompt
2838
+ // won't include) is excluded. Reusing the auto-continue recovery band
2839
+ // here turned recoverable overflows into manual dead-ends (#3412 review),
2840
+ // so use the looser fit budget.
2841
+ retryFits = this.#compactionCreatedRetryFit();
2842
+ if (!retryFits) {
2843
+ retryFits = await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
2844
+ skipElide: fallbackFromShake,
2845
+ hasProgress: () => this.#compactionCreatedRetryFit(),
2846
+ });
2847
+ }
2848
+ if (!retryFits) {
2849
+ noProgressDeadEnd = true;
2850
+ }
2851
+ } else if (reason !== "idle") {
2852
+ // Mirror the shake recovery-band check: only auto-continue when compaction
2853
+ // landed residual context under `COMPACTION_RECOVERY_BAND × threshold`.
2854
+ // Re-firing on a history that still sits just over the line is the
2855
+ // snapcompact thrash, so require genuine headroom, not a bare fit. Even
2856
+ // when auto-continue is disabled, a no-headroom threshold pass must still
2857
+ // block later automatic continuations (todo reminders/session_stop hooks)
2858
+ // from re-entering the same oversized context.
2859
+ hasHeadroom = this.#compactionCreatedHeadroom();
2860
+ if (!hasHeadroom) {
2861
+ hasHeadroom = await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
2862
+ skipElide: fallbackFromShake,
2863
+ hasProgress: () => this.#compactionCreatedHeadroom(),
2864
+ });
2865
+ }
2866
+ if (!hasHeadroom) {
2867
+ noProgressDeadEnd = true;
2868
+ }
2869
+ }
2870
+
2871
+ const deadEndWarning = noProgressDeadEnd ? compactionDeadEndWarning("clear large tool output") : undefined;
2872
+ if (deadEndWarning) {
2873
+ // Stamp the divider: the compaction bar badges the dead-end and
2874
+ // carries the full warning in its ctrl+o detail, so the pause
2875
+ // stays explained even after the notice row scrolls away. Stamp
2876
+ // the branch's LATEST compaction entry — a frame rescue may have
2877
+ // superseded `savedCompactionEntry` with a rebuilt one, and the
2878
+ // collapsed transcript badges only the active entry.
2879
+ const stampEntry = getLatestCompactionEntry(this.#host.sessionManager.getBranch()) ?? savedCompactionEntry;
2880
+ if (stampEntry) {
2881
+ stampEntry.warning = deadEndWarning;
2882
+ await this.#host.sessionManager.rewriteEntries();
2883
+ }
2884
+ }
2885
+
2886
+ await this.#host.emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
2887
+
2888
+ if (retryFits) {
2889
+ this.#host.scheduleAgentContinue({ delayMs: 100, generation });
2890
+ continuationScheduled = true;
2891
+ } else {
2892
+ continuationScheduled = this.#host.scheduleCompactionContinuation({
2893
+ generation,
2894
+ autoContinue: hasHeadroom && shouldAutoContinue,
2895
+ terminalTextAnswer,
2896
+ suppressContinuation,
2897
+ });
2898
+ }
2899
+
2900
+ if (deadEndWarning) {
2901
+ this.#host.emitNotice("warning", deadEndWarning, "compaction");
2902
+ }
2903
+ if (continuationScheduled) return COMPACTION_CHECK_CONTINUATION;
2904
+ return noProgressDeadEnd ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION : COMPACTION_CHECK_NONE;
2905
+ } catch (error) {
2906
+ if (autoCompactionSignal.aborted) {
2907
+ await this.#host.emitSessionEvent({
2908
+ type: "auto_compaction_end",
2909
+ action,
2910
+ result: undefined,
2911
+ aborted: true,
2912
+ willRetry: false,
2913
+ });
2914
+ return COMPACTION_CHECK_NONE;
2915
+ }
2916
+ const errorMessage = error instanceof Error ? error.message : "compaction failed";
2917
+ await this.#host.emitSessionEvent({
2918
+ type: "auto_compaction_end",
2919
+ action,
2920
+ result: undefined,
2921
+ aborted: false,
2922
+ willRetry: false,
2923
+ errorMessage:
2924
+ reason === "overflow"
2925
+ ? `Context overflow recovery failed: ${errorMessage}`
2926
+ : reason === "incomplete"
2927
+ ? `Incomplete response recovery failed: ${errorMessage}`
2928
+ : `Auto-compaction failed: ${errorMessage}`,
2929
+ });
2930
+ } finally {
2931
+ if (this.#autoCompactionAbortController === autoCompactionAbortController) {
2932
+ this.#autoCompactionAbortController = undefined;
2933
+ }
2934
+ }
2935
+ return COMPACTION_CHECK_NONE;
2936
+ }
2937
+
2938
+ /**
2939
+ * Run a shake-strategy auto-maintenance pass. Emits the
2940
+ * `auto_compaction_start`/`auto_compaction_end` pair with a shake `action`,
2941
+ * runs {@link shake} inline against the protect-window config, and schedules
2942
+ * continuation exactly like the context-full tail.
2943
+ *
2944
+ * Returns `"fallback"` only for an overflow recovery where shake reclaimed
2945
+ * nothing (or threw) — the caller then runs the summary-compaction body so
2946
+ * the oversized input still gets resolved. Returns `"handled"` otherwise.
2947
+ */
2948
+ async #runAutoShake(
2949
+ reason: "overflow" | "threshold" | "idle" | "incomplete",
2950
+ willRetry: boolean,
2951
+ generation: number,
2952
+ autoContinue: boolean,
2953
+ terminalTextAnswer: boolean,
2954
+ triggerContextTokens?: number,
2955
+ suppressContinuation = false,
2956
+ ): Promise<CompactionCheckResult | "fallback"> {
2957
+ const action = "shake";
2958
+ this.#autoCompactionAbortController?.abort();
2959
+ const controller = new AbortController();
2960
+ this.#autoCompactionAbortController = controller;
2961
+ const signal = controller.signal;
2962
+ try {
2963
+ await this.#host.emitSessionEvent({ type: "auto_compaction_start", reason, action });
2964
+ const result = await this.#host.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
2965
+ if (signal.aborted) {
2966
+ await this.#host.emitSessionEvent({
2967
+ type: "auto_compaction_end",
2968
+ action,
2969
+ result: undefined,
2970
+ aborted: true,
2971
+ willRetry: false,
2972
+ });
2973
+ return COMPACTION_CHECK_NONE;
2974
+ }
2975
+ const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
2976
+ // Detect the dead-loop reported in issues #2119/#2275: the threshold check
2977
+ // fires, shake runs, but residual context is still above the configured
2978
+ // threshold. The next agent_end would re-trigger shake, which has nothing
2979
+ // new to drop on the second pass, so the loop spins until the user kills it.
2980
+ // Same hazard for "incomplete" (the retry would re-hit the length cap) and
2981
+ // for the existing "overflow + nothing reclaimed" case. In every recovery
2982
+ // reason we hand off to the summarization-driven context-full path so the
2983
+ // situation actually resolves; "idle" is exempt because its 60s+ timer
2984
+ // re-checks usage before re-firing and cannot dead-loop on its own.
2985
+ //
2986
+ // #2275: the post-shake check MUST stay provider-anchored when caller
2987
+ // usage and local estimates diverge. The local estimator undercounts
2988
+ // thinking-signature payloads, so thinking-heavy sessions can read well
2989
+ // below the provider usage that fired the threshold. Prefer the caller's
2990
+ // context figure when supplied, then subtract shake's own savings and add
2991
+ // hysteresis (80% recovery band) so we don't oscillate at the boundary.
2992
+ // Threshold callers pass the provider-billed trigger after accounting for
2993
+ // any supersede/drop-useless pruning that already rewrote the next prompt;
2994
+ // without that pre-shake savings, shake can fall through to context-full
2995
+ // even though the post-prune history is already inside the recovery band.
2996
+ const contextWindow = this.#model?.contextWindow ?? 0;
2997
+ const compactionSettings = this.#host.settings.getGroup("compaction");
2998
+ let stillOverThreshold = false;
2999
+ if (contextWindow > 0) {
3000
+ if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
3001
+ const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
3002
+ const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
3003
+ const recoveryBand = Math.floor(thresholdTokens * COMPACTION_RECOVERY_BAND);
3004
+ stillOverThreshold = correctedTokens > recoveryBand;
3005
+ } else {
3006
+ const postShakeTokens = this.#host.getContextUsage({ contextWindow })?.tokens ?? 0;
3007
+ stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
3008
+ }
3009
+ }
3010
+ const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
3011
+ if (shouldFallBack) {
3012
+ const errorMessage = reclaimed
3013
+ ? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
3014
+ : "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
3015
+ await this.#host.emitSessionEvent({
3016
+ type: "auto_compaction_end",
3017
+ action,
3018
+ result: undefined,
3019
+ aborted: false,
3020
+ willRetry: false,
3021
+ skipped: !reclaimed,
3022
+ errorMessage,
3023
+ });
3024
+ return "fallback";
3025
+ }
3026
+ await this.#host.emitSessionEvent({
3027
+ type: "auto_compaction_end",
3028
+ action,
3029
+ result: undefined,
3030
+ aborted: false,
3031
+ willRetry,
3032
+ skipped: !reclaimed,
3033
+ });
3034
+
3035
+ let continuationScheduled = false;
3036
+ if (willRetry) {
3037
+ // The shake rebuild replays every entry, so a trailing error/length
3038
+ // assistant from the failed turn re-enters agent state — drop it before
3039
+ // retrying, same as the context-full tail.
3040
+ const messages = this.#host.agent.state.messages;
3041
+ const lastMsg = messages[messages.length - 1];
3042
+ if (lastMsg?.role === "assistant") {
3043
+ const lastAssistant = lastMsg as AssistantMessage;
3044
+ const shouldDrop =
3045
+ lastAssistant.stopReason === "error" ||
3046
+ (reason === "incomplete" && lastAssistant.stopReason === "length");
3047
+ if (shouldDrop) this.#host.agent.replaceMessages(messages.slice(0, -1));
3048
+ }
3049
+ this.#host.scheduleAgentContinue({ delayMs: 100, generation });
3050
+ continuationScheduled = true;
3051
+ } else {
3052
+ continuationScheduled = this.#host.scheduleCompactionContinuation({
3053
+ generation,
3054
+ autoContinue: reason !== "idle" && autoContinue,
3055
+ terminalTextAnswer,
3056
+ suppressContinuation,
3057
+ });
3058
+ }
3059
+ if (!reclaimed) {
3060
+ return willRetry && continuationScheduled
3061
+ ? { ...COMPACTION_CHECK_CONTINUATION, historyRewritten: true }
3062
+ : continuationScheduled
3063
+ ? COMPACTION_CHECK_CONTINUATION
3064
+ : COMPACTION_CHECK_NONE;
3065
+ }
3066
+ return {
3067
+ ...(continuationScheduled ? COMPACTION_CHECK_CONTINUATION : COMPACTION_CHECK_NONE),
3068
+ historyRewritten: true,
3069
+ };
3070
+ } catch (error) {
3071
+ if (signal.aborted) {
3072
+ await this.#host.emitSessionEvent({
3073
+ type: "auto_compaction_end",
3074
+ action,
3075
+ result: undefined,
3076
+ aborted: true,
3077
+ willRetry: false,
3078
+ });
3079
+ return COMPACTION_CHECK_NONE;
3080
+ }
3081
+ const message = error instanceof Error ? error.message : "shake failed";
3082
+ await this.#host.emitSessionEvent({
3083
+ type: "auto_compaction_end",
3084
+ action,
3085
+ result: undefined,
3086
+ aborted: false,
3087
+ willRetry: false,
3088
+ errorMessage: message,
3089
+ skipped: false,
3090
+ });
3091
+ // Overflow still needs recovery even if shake threw.
3092
+ return reason === "overflow" ? "fallback" : COMPACTION_CHECK_NONE;
3093
+ } finally {
3094
+ if (this.#autoCompactionAbortController === controller) {
3095
+ this.#autoCompactionAbortController = undefined;
3096
+ }
3097
+ }
3098
+ }
3099
+
3100
+ /**
3101
+ * Toggle auto-compaction setting.
3102
+ */
3103
+ setAutoCompactionEnabled(enabled: boolean): void {
3104
+ this.#host.settings.set("compaction.enabled", enabled);
3105
+ if (enabled && this.#host.settings.get("compaction.strategy") === "off") {
3106
+ const defaultStrategy = getDefault("compaction.strategy");
3107
+ this.#host.settings.set("compaction.strategy", defaultStrategy === "off" ? "context-full" : defaultStrategy);
3108
+ }
3109
+ }
3110
+
3111
+ /** Whether auto-compaction is enabled */
3112
+ get autoCompactionEnabled(): boolean {
3113
+ return this.#host.settings.get("compaction.enabled") && this.#host.settings.get("compaction.strategy") !== "off";
3114
+ }
3115
+ }