one-code-extension 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +219 -219
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +66 -34
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Where a compaction cuts the conversation. Pure: no pi extension API.
3
+ *
4
+ * pi keeps the last ~20k tokens verbatim after its summary (`keepRecentTokens`).
5
+ * Claude Code keeps only the final assistant reply (findings §47), and
6
+ * so does One Code: the summary, that reply, then whatever came after it (the
7
+ * results of a tool call it made, on an overflow mid-turn). A short kept tail
8
+ * also keeps little signed thinking across the boundary (findings §46).
9
+ *
10
+ * pi's own cut is moved to the last assistant message that reaches the
11
+ * provider. When pi cut earlier (a keep window above 0), the messages between
12
+ * the two cuts join the doomed span. When pi cut later (with the window at 0,
13
+ * pi cuts at the last cut point, which can be a queued prompt or a harness
14
+ * notification that arrived after the reply), the reply and what follows it
15
+ * leave the doomed span, so the summary does not repeat what stays verbatim.
16
+ */
17
+
18
+ import { latestCompaction } from "../lib/compaction-boundary.ts";
19
+
20
+ /** The fields of a session entry the cut reads; pi's `SessionEntry` fits it. */
21
+ export type CutEntry = {
22
+ id: string;
23
+ type: string;
24
+ message?: { role?: string; stopReason?: string; content?: unknown };
25
+ targetId?: string;
26
+ replacement?: { content: unknown } | null;
27
+ firstKeptEntryId?: string | null;
28
+ };
29
+
30
+ type Span<M> = { messagesToSummarize: M[]; turnPrefixMessages: M[]; isSplitTurn: boolean };
31
+ type Preparation<M> = Span<M> & { firstKeptEntryId: string };
32
+
33
+ /** What pi summarizes for a preparation: the messages before the cut, a split turn's prefix included. */
34
+ export function doomedMessages<M>(preparation: Span<M>): M[] {
35
+ return preparation.isSplitTurn ? [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages] : preparation.messagesToSummarize;
36
+ }
37
+
38
+ /**
39
+ * pi's preparation moved to Claude Code's cut, with the entry the kept tail
40
+ * starts at. pi's own when its cut already is the reply, no reply is still in
41
+ * context, or the messages to take off are not the doomed span's tail.
42
+ */
43
+ export function claudeCodeCut<E extends CutEntry, M extends { role: string }, P extends Preparation<M>>(
44
+ entries: readonly E[],
45
+ preparation: P,
46
+ toMessages: (entry: E) => M[],
47
+ ): { preparation: P; firstKeptEntryId: string } {
48
+ const unchanged = { preparation, firstKeptEntryId: preparation.firstKeptEntryId };
49
+ const cut = lastReplyCut(entries, preparation.firstKeptEntryId);
50
+ if (!cut) return unchanged;
51
+ const moved = moveCut(preparation, spanMessages(cut.span, entries, toMessages), cut.later);
52
+ return moved ? { preparation: moved, firstKeptEntryId: cut.firstKeptEntryId } : unchanged;
53
+ }
54
+
55
+ /**
56
+ * The entry Claude Code's cut keeps from, and the entries between it and pi's
57
+ * cut: summarized too when the reply is `later` than pi's cut, kept when it is
58
+ * earlier. Undefined when pi's entry is not on the branch, no reply is still
59
+ * in context, or pi already cut at the reply.
60
+ *
61
+ * An assistant message that ended in an error or an abort never reaches the
62
+ * provider (pi-ai's `transformMessages` drops it), and one whose content (as
63
+ * a context edit leaves it) has no text or tool call is omitted or skipped by
64
+ * its converter, so none of them is a reply to keep. A reply an
65
+ * earlier compaction already summarized is out of reach.
66
+ */
67
+ export function lastReplyCut<E extends CutEntry>(
68
+ entries: readonly E[],
69
+ piFirstKeptEntryId: string,
70
+ ): { firstKeptEntryId: string; span: E[]; later: boolean } | undefined {
71
+ const piIndex = entries.findIndex((entry) => entry.id === piFirstKeptEntryId);
72
+ if (piIndex < 0) return undefined;
73
+ const edits = contextEdits(entries);
74
+ const start = latestCompaction(entries)?.keptStart ?? 0;
75
+ let replyIndex = -1;
76
+ for (let i = entries.length - 1; i >= start; i--) {
77
+ const entry = entries[i];
78
+ const message = entry.message;
79
+ if (entry.type !== "message" || message?.role !== "assistant") continue;
80
+ if (message.stopReason === "error" || message.stopReason === "aborted") continue;
81
+ // The content the provider gets: a context edit's replacement, or none when it omits the message.
82
+ const content = edits.has(entry.id) ? edits.get(entry.id)?.content : message.content;
83
+ if (!sendsContent(content)) continue;
84
+ replyIndex = i;
85
+ break;
86
+ }
87
+ if (replyIndex < 0 || replyIndex === piIndex) return undefined;
88
+ const later = replyIndex > piIndex;
89
+ return {
90
+ firstKeptEntryId: entries[replyIndex].id,
91
+ span: later ? entries.slice(piIndex, replyIndex) : entries.slice(replyIndex, piIndex),
92
+ later,
93
+ };
94
+ }
95
+
96
+ /** Each edited entry's replacement (null when the edit omits it), the latest edit winning, as in pi's projection. */
97
+ function contextEdits(entries: readonly CutEntry[]): Map<string, CutEntry["replacement"]> {
98
+ const edits = new Map<string, CutEntry["replacement"]>();
99
+ for (const entry of entries) if (entry.type === "context_edit" && entry.targetId) edits.set(entry.targetId, entry.replacement);
100
+ return edits;
101
+ }
102
+
103
+ /** Whether an assistant message carries text or a tool call (thinking alone is not a reply). */
104
+ function sendsContent(content: unknown): boolean {
105
+ if (typeof content === "string") return content.trim().length > 0;
106
+ return (
107
+ Array.isArray(content) &&
108
+ content.some((block: { type?: string; text?: string }) => block?.type === "toolCall" || (block?.type === "text" && !!block.text?.trim()))
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The context messages of entries in branch order, as pi projects them: an
114
+ * omitted entry contributes nothing, a replaced one its replacement content
115
+ * (`buildSessionProjection` in pi 0.87 and later; earlier pi has no edits), and
116
+ * an older compaction entry inside the span nothing (only the newest
117
+ * compaction's summary is context).
118
+ */
119
+ export function spanMessages<E extends CutEntry, M extends { role: string }>(span: readonly E[], all: readonly E[], toMessages: (entry: E) => M[]): M[] {
120
+ const edits = contextEdits(all);
121
+ return span.flatMap((entry) => {
122
+ if (entry.type === "compaction" || entry.type === "context_edit") return [];
123
+ const messages = toMessages(entry);
124
+ if (!edits.has(entry.id)) return messages;
125
+ const replacement = edits.get(entry.id);
126
+ if (!replacement) return [];
127
+ return messages.map((message) => {
128
+ if (!["user", "assistant", "toolResult", "custom"].includes(message.role)) return message;
129
+ const content =
130
+ (message.role === "assistant" || message.role === "toolResult") && typeof replacement.content === "string"
131
+ ? [{ type: "text", text: replacement.content }]
132
+ : replacement.content;
133
+ return { ...message, content };
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * pi's preparation with the doomed span moved to the new cut, the split turn's
140
+ * prefix folded in: `messages` appended when the reply is `later` than pi's
141
+ * cut, else taken off the span's end. Undefined when those are not the span's
142
+ * tail (role and timestamp, message by message) or would leave nothing to
143
+ * summarize: pi's cut stays.
144
+ */
145
+ export function moveCut<P extends Span<unknown>>(preparation: P, messages: P["messagesToSummarize"], later: boolean): P | undefined {
146
+ const doomed = doomedMessages(preparation);
147
+ const moved = (messagesToSummarize: P["messagesToSummarize"]): P => ({ ...preparation, messagesToSummarize, turnPrefixMessages: [], isSplitTurn: false });
148
+ if (later) return moved([...doomed, ...messages]);
149
+ const keep = doomed.length - messages.length;
150
+ if (keep <= 0) return undefined;
151
+ const same = (a: unknown, b: unknown) => {
152
+ const [x, y] = [a as { role?: string; timestamp?: number } | null, b as { role?: string; timestamp?: number } | null];
153
+ return x?.role === y?.role && x?.timestamp === y?.timestamp;
154
+ };
155
+ if (messages.some((message, i) => !same(message, doomed[keep + i]))) return undefined;
156
+ return moved(doomed.slice(0, keep));
157
+ }
@@ -2,9 +2,9 @@
2
2
  * compaction extension — replaces pi's summarizer with Claude Code's.
3
3
  *
4
4
  * Hooks `session_before_compact` (manual /compact, threshold, and overflow
5
- * alike), reuses pi's already-computed cut point, and sends Claude Code's
6
- * compaction instruction; only the <summary> block of the reply survives as
7
- * context.
5
+ * alike), moves pi's cut point to the last assistant reply (`cut.ts`), and
6
+ * sends Claude Code's compaction instruction; only the <summary> block of the
7
+ * reply survives as context, followed by that reply.
8
8
  *
9
9
  * The call is built to hit the provider prompt cache, which is why Claude
10
10
  * Code compacts on the *session model* rather than something cheaper: it
@@ -41,20 +41,25 @@ import { completeSimple } from "@earendil-works/pi-ai/compat";
41
41
  import {
42
42
  convertToLlm,
43
43
  type ExtensionAPI,
44
- type ExtensionContext,
45
44
  type SessionBeforeCompactEvent,
45
+ type SessionEntry,
46
+ sessionEntryToContextMessages,
46
47
  } from "@earendil-works/pi-coding-agent";
47
48
  import {
48
49
  type AnthropicModelCompat,
49
- anthropicBetas,
50
50
  clearThinkingApplies,
51
51
  clearThinkingEnabled,
52
- isAnthropicOAuth,
52
+ sessionRequestHeaders,
53
53
  withClearThinking,
54
54
  } from "../context-management/index.ts";
55
+ import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "../hooks/compaction.ts";
55
56
  import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
56
57
  import { forcedReasoningLevel } from "../lib/model-policy.ts";
58
+ import { captureRequest, extendPayload, LAST_REQUEST_CHANNEL, LastExchange, lastCovered, type RequestCapture, replayOutputCap } from "../lib/request-replay.ts";
59
+ import { withoutSystemMessages } from "../lib/side-call.ts";
60
+ import { claudeCodeCut, type CutEntry, doomedMessages } from "./cut.ts";
57
61
  import { fitToBudget, replayFits, withoutUsage } from "./fit.ts";
62
+ import { withoutThinking } from "./kept-thinking.ts";
58
63
  import { buildCompactionInstruction, COMPACTION_MAX_TOKENS, continuationSummary, extractSummary } from "./prompt.ts";
59
64
 
60
65
  /** Compaction reads a whole context window; give it more room than the classifier's 30s. */
@@ -71,6 +76,11 @@ export const STANDALONE_SYSTEM_PROMPT =
71
76
  type Preparation = Pick<SessionBeforeCompactEvent["preparation"], "messagesToSummarize" | "turnPrefixMessages" | "isSplitTurn" | "previousSummary">;
72
77
 
73
78
  export default function compactionExtension(pi: ExtensionAPI) {
79
+ const hookInstructions = new WeakMap<AbortSignal, string>();
80
+ pi.events.on(PRECOMPACT_INSTRUCTIONS_CHANNEL, (data) => {
81
+ const { signal, instructions } = data as PreCompactInstructions;
82
+ hookInstructions.set(signal, instructions);
83
+ });
74
84
  /**
75
85
  * The exact message array the session last handed the provider, captured
76
86
  * from the `context` event AFTER every extension has mutated it — most
@@ -87,38 +97,83 @@ export default function compactionExtension(pi: ExtensionAPI) {
87
97
  * standalone path) diverges at message one, because those injected reminders
88
98
  * never become entries — so it cannot reproduce the cached prefix at all.
89
99
  *
90
- * We hold the reference, not a copy: `emitContext` hands each handler a fresh
91
- * structuredClone the session never mutates again, and compaction is the
92
- * last extension with a `context` handler, so this reference is exactly the
93
- * array the turn sent. Returning nothing keeps the handler a pure observer.
100
+ * We hold the messages themselves, not copies: `emitContext` hands each
101
+ * handler a fresh structuredClone the session never mutates again, and
102
+ * compaction is the last extension with a `context` handler, so these are
103
+ * exactly the messages the turn sent. Returning nothing keeps the handler a
104
+ * pure observer.
105
+ *
106
+ * The system prompt is captured at the same moment. `ctx.getSystemPrompt()`
107
+ * reflects the prompt a run forced through `before_agent_start` only while
108
+ * that run lasts; pi drops the run's options when it settles, so read from an
109
+ * idle `/compact` it is pi's default prompt and the replay misses the cache
110
+ * (findings §27).
111
+ */
112
+ let captured: { messages: AgentMessage[]; systemPrompt: string; model: string | undefined } | undefined;
113
+ pi.on("context", (event, ctx) => {
114
+ captured = { messages: event.messages, systemPrompt: ctx.getSystemPrompt(), model: modelKey(ctx.model) };
115
+ });
116
+
117
+ /**
118
+ * The same request as pi put it on the wire, and the reply that answered it
119
+ * (lib/request-replay.ts). This extension loads after every extension that
120
+ * edits the payload (tool-search, context-management), so the body it sees
121
+ * is final; it is published for the btw and recap side calls too.
94
122
  */
95
- let capturedMessages: AgentMessage[] | undefined;
96
- pi.on("context", (event) => {
97
- capturedMessages = event.messages;
123
+ const exchange = new LastExchange<AgentMessage & { role: string }>();
124
+ const publish = (capture: ReturnType<typeof captureRequest>) => {
125
+ exchange.setCapture(capture);
126
+ pi.events.emit(LAST_REQUEST_CHANNEL, capture);
127
+ };
128
+ pi.on("before_provider_request", (event, ctx) => {
129
+ // The context this body was built from ends where a fork's own messages start.
130
+ const covers = captured ? lastCovered(captured.messages as { role: string; timestamp?: number }[]) : undefined;
131
+ publish(captureRequest(ctx.model as { api?: string; provider?: string; id?: string } | undefined, event.payload, covers, ctx.sessionManager.getSessionId()));
132
+ });
133
+ pi.on("message_end", (event) => {
134
+ exchange.noteMessage(event.message as AgentMessage & { role: string });
135
+ });
136
+ pi.on("session_start", () => {
137
+ captured = undefined;
138
+ publish(undefined);
98
139
  });
140
+
99
141
  // After a compaction the capture describes the pre-compaction request. A
100
142
  // second /compact before any turn would otherwise re-summarize history the
101
143
  // first one already folded away (and mis-scope the kept tail); the standalone
102
144
  // reconstruction from entries serves until the next request recaptures.
103
145
  pi.on("session_compact", () => {
104
- capturedMessages = undefined;
146
+ captured = undefined;
147
+ publish(undefined);
105
148
  });
106
149
  // A /tree branch switch stays in the same process and fires no `context`
107
150
  // event (navigateTree rebuilds the messages itself), so the capture would
108
151
  // still describe the abandoned branch — and a /compact before the next turn
109
152
  // summarized work the kept branch never did (review H1). Same remedy.
110
153
  pi.on("session_tree", () => {
111
- capturedMessages = undefined;
154
+ captured = undefined;
155
+ publish(undefined);
112
156
  });
113
157
 
114
158
  pi.on("session_before_compact", async (event, ctx) => {
159
+ const instructions = hookInstructions.get(event.signal);
160
+ hookInstructions.delete(event.signal);
161
+ if (instructions) event = { ...event, customInstructions: [event.customInstructions, instructions].filter(Boolean).join("\n\n") };
115
162
  if (process.env.CC_COMPACTION === "0") return undefined;
116
163
 
117
164
  const model = ctx.model;
118
165
  if (!model) return undefined;
119
166
  // Snapshot before the first await so the capture and pi's preparation
120
167
  // describe the same request even if a context event lands meanwhile.
121
- const captured = capturedMessages;
168
+ // A replay exists to read the session model's cache, and the prompt is built
169
+ // per model, so a capture made under another model (a `/model` switch with
170
+ // no turn since) is not replayed: the standalone shape serves.
171
+ const capture =
172
+ captured && captured.model === modelKey(model) ? { ...captured, messages: withoutSystemMessages(captured.messages) } : undefined;
173
+ const wire = exchange.forModel(model);
174
+ // Claude Code's cut: keep only the last assistant reply, and summarize
175
+ // exactly what lies before it (cut.ts).
176
+ const { preparation, firstKeptEntryId } = claudeCodeCut(event.branchEntries as (SessionEntry & CutEntry)[], event.preparation, sessionEntryToContextMessages);
122
177
 
123
178
  try {
124
179
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
@@ -133,16 +188,24 @@ export default function compactionExtension(pi: ExtensionAPI) {
133
188
  // Otherwise (no capture yet, an overflow, a nearly full window) the
134
189
  // standalone shape summarizes the doomed span pi isolated. An overflow
135
190
  // never replays, so its replay request is not even built.
136
- const replay = captured && event.reason !== "overflow" ? replayRequest(pi, ctx, captured, event) : undefined;
137
- const request =
138
- replay && replayFits(event.reason, model, replay, maxTokens)
139
- ? replay
140
- : standaloneRequest(
141
- model,
142
- event.preparation,
143
- buildCompactionInstruction({ reason: event.reason, customInstructions: event.customInstructions }),
144
- maxTokens,
145
- );
191
+ const replay = capture && event.reason !== "overflow" ? replayRequest(pi, capture, event, preparation, wire?.reply, model.api) : undefined;
192
+ const replaying = replay !== undefined && replayFits(event.reason, model, replay, maxTokens);
193
+ const request = replaying
194
+ ? replay
195
+ : standaloneRequest(
196
+ model,
197
+ preparation,
198
+ buildCompactionInstruction({ reason: event.reason, customInstructions: event.customInstructions }),
199
+ maxTokens,
200
+ );
201
+ // A replay goes out as the session's own last request body with the
202
+ // reply and the instruction appended, so it reads the cache that request
203
+ // wrote: rebuilt from `request`, the tools array (tool-search reshapes it
204
+ // on the wire) never matched on first-party Claude (measured on Sonnet 5:
205
+ // 0 read, 65k written). `request` still sizes the call (fit.ts), and
206
+ // pi-ai converts only the tail. No capture for this model, or too little
207
+ // room left under the captured cap: `request` as is.
208
+ const plan = replaying && wire ? wirePlan(wire, request, maxTokens) : undefined;
146
209
 
147
210
  // When context-management (clear_thinking) is active for this session,
148
211
  // the agent loop's cached message prefix has old thinking blocks cleared
@@ -159,17 +222,32 @@ export default function compactionExtension(pi: ExtensionAPI) {
159
222
  // the Anthropic-shaped fields, and are gated to Anthropic anyway.
160
223
  const cmModel = model as { api?: string; provider?: string; baseUrl?: string; compat?: AnthropicModelCompat };
161
224
  const clearThinking = clearThinkingEnabled(process.env.CC_CLEAR_THINKING, cmModel);
162
- const headers = clearThinking
163
- ? { ...auth.headers, "anthropic-beta": anthropicBetas(isAnthropicOAuth(), cmModel.compat) }
164
- : auth.headers;
225
+ const headers = sessionRequestHeaders(cmModel, auth.headers);
226
+
227
+ // A replay sends the captured body (which already carries the
228
+ // clear_thinking edit) with the tail pi-ai just built appended.
229
+ // Otherwise the clear_thinking body edit, matching the session's
230
+ // requests (see the header note above), only on Anthropic requests
231
+ // that carry thinking, exactly as the context-management extension gates it.
232
+ let onPayload: ((payload: unknown) => unknown) | undefined;
233
+ if (plan) {
234
+ onPayload = (payload) => extendPayload(plan.capture, payload as Record<string, unknown>, plan.maxTokens);
235
+ } else if (clearThinking) {
236
+ onPayload = (payload) =>
237
+ looksLikeAnthropicRequest(payload) &&
238
+ clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
239
+ ? withClearThinking(payload as Record<string, unknown>)
240
+ : payload;
241
+ }
165
242
 
166
243
  const timeout = AbortSignal.timeout(COMPACTION_TIMEOUT_MS);
167
- const result = await completeSimple(baseUrl ? ({ ...model, baseUrl } as Model<Api>) : model, request, {
244
+ const callContext: Context = plan ? { systemPrompt: "", messages: plan.tail, tools: [] } : request;
245
+ const result = await completeSimple(baseUrl ? ({ ...model, baseUrl } as Model<Api>) : model, callContext, {
168
246
  apiKey: auth.apiKey,
169
247
  headers,
170
248
  env: auth.env,
171
249
  signal: AbortSignal.any([event.signal, timeout]),
172
- maxTokens,
250
+ maxTokens: plan?.maxTokens ?? maxTokens,
173
251
  // The session id is the prompt-cache key / routing affinity on the
174
252
  // providers that use one (openai-codex prompt_cache_key, session
175
253
  // headers). Without it the replayed prefix cannot hit the session's
@@ -189,18 +267,13 @@ export default function compactionExtension(pi: ExtensionAPI) {
189
267
  // unless the model cannot disable thinking, where an off-request
190
268
  // would 400 (forcedReasoningLevel sends its lowest level instead).
191
269
  reasoning: ctx.thinkingLevel === "off" ? forcedReasoningLevel(model) : ctx.thinkingLevel,
192
- // The clear_thinking body edit, matching the session's requests (see
193
- // the header note above). Only attached on Anthropic requests that
194
- // carry thinking, exactly as the context-management extension gates it.
195
- onPayload: clearThinking
196
- ? (payload: unknown) =>
197
- looksLikeAnthropicRequest(payload) &&
198
- clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
199
- ? withClearThinking(payload as Record<string, unknown>)
200
- : payload
201
- : undefined,
270
+ onPayload,
202
271
  });
203
272
 
273
+ // A failed or capped stream can carry partial text. Never replace the
274
+ // conversation with that incomplete checkpoint; let pi retry its summary.
275
+ if (result.stopReason === "error" || result.stopReason === "aborted" || result.stopReason === "length") return undefined;
276
+
204
277
  const text = result.content
205
278
  .filter((block): block is { type: "text"; text: string } => block.type === "text")
206
279
  .map((block) => block.text)
@@ -212,7 +285,7 @@ export default function compactionExtension(pi: ExtensionAPI) {
212
285
  compaction: {
213
286
  // Undefined in --no-session runs; the pointer line is dropped.
214
287
  summary: continuationSummary(summary, ctx.sessionManager.getSessionFile()),
215
- firstKeptEntryId: event.preparation.firstKeptEntryId,
288
+ firstKeptEntryId,
216
289
  tokensBefore: event.preparation.tokensBefore,
217
290
  usage: result.usage,
218
291
  },
@@ -224,20 +297,50 @@ export default function compactionExtension(pi: ExtensionAPI) {
224
297
  });
225
298
  }
226
299
 
300
+ /** A model's identity for matching a capture to the current model. */
301
+ function modelKey(model: { provider?: string; id?: string } | undefined): string | undefined {
302
+ return model ? `${model.provider}/${model.id}` : undefined;
303
+ }
304
+
305
+ /**
306
+ * A replay's wire form: the tail pi-ai converts (the reply, if any, and the
307
+ * instruction: the last messages of `request`) and the output cap left under
308
+ * the captured request's own, or undefined when too little room is left.
309
+ */
310
+ function wirePlan(
311
+ wire: { capture: RequestCapture; reply: unknown },
312
+ request: Context,
313
+ maxTokens: number,
314
+ ): { capture: RequestCapture; tail: Message[]; maxTokens: number } | undefined {
315
+ const tail = request.messages.slice(-(wire.reply ? 2 : 1));
316
+ const cap = replayOutputCap(wire.capture, tail, maxTokens);
317
+ return cap === undefined ? undefined : { capture: wire.capture, tail, maxTokens: cap };
318
+ }
319
+
227
320
  /**
228
321
  * The cache-aligned shape: the session's system prompt, the active tool
229
322
  * definitions in their active order — kept purely so the cached prefix (tools
230
323
  * come first) still matches; a mismatch costs cache hits, never correctness —
231
324
  * the captured request verbatim, and the instruction as one more user message.
232
325
  *
233
- * pi keeps the tail after the cut point verbatim (keepRecentTokens, ~20k
234
- * tokens); the captured request still carries that tail, so the instruction
235
- * names it (`keptTailOf`). Cutting the tail off the request instead was
326
+ * The kept tail (the last assistant reply and anything after it, `cut.ts`)
327
+ * stays verbatim after the summary; the captured request still carries it, so
328
+ * the instruction names it (`keptTailOf`). Cutting the tail off the request instead was
236
329
  * rejected: Anthropic checks cache hits only ~20 blocks back from the
237
330
  * breakpoint, so a request ending well before the last cached block misses
238
331
  * the cache the whole replay exists to hit.
239
332
  */
240
- function replayRequest(pi: ExtensionAPI, ctx: ExtensionContext, captured: AgentMessage[], event: SessionBeforeCompactEvent): Context {
333
+ function replayRequest(
334
+ pi: ExtensionAPI,
335
+ captured: { messages: AgentMessage[]; systemPrompt: string },
336
+ event: SessionBeforeCompactEvent,
337
+ preparation: Preparation,
338
+ reply: AgentMessage | undefined,
339
+ api: string | undefined,
340
+ ): Context {
341
+ // The reply that answered the captured request is part of the conversation
342
+ // pi is compacting, and the last message the replayed body gains.
343
+ const conversation = reply ? [...captured.messages, reply] : captured.messages;
241
344
  const byName = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
242
345
  const tools = pi
243
346
  .getActiveTools()
@@ -247,25 +350,38 @@ function replayRequest(pi: ExtensionAPI, ctx: ExtensionContext, captured: AgentM
247
350
  const instruction = buildCompactionInstruction({
248
351
  reason: event.reason,
249
352
  customInstructions: event.customInstructions,
250
- keptTail: keptTailOf(captured, event.preparation),
353
+ keptTail: keptTailOf(conversation, preparation, api),
251
354
  });
252
- return { systemPrompt: ctx.getSystemPrompt(), messages: [...convertToLlm(captured), instructionMessage(instruction)], tools };
355
+ return { systemPrompt: captured.systemPrompt, messages: [...convertToLlm(conversation), instructionMessage(instruction)], tools };
253
356
  }
254
357
 
255
358
  /**
256
359
  * The standalone shape: the doomed span reconstructed from session entries,
257
- * its assistant usage zeroed and its largest tool results cleared until pi's
258
- * clamp leaves room for the summary (`fit.ts`), no tools, a one-line system
259
- * prompt. Gets no kept-tail note: it holds only the doomed span. If it fails,
360
+ * its assistant usage zeroed, its signed thinking removed (it would fail the
361
+ * check under this system prompt; `kept-thinking.ts`), and its largest tool
362
+ * results cleared until pi's clamp leaves room for the summary (`fit.ts`), no
363
+ * tools, a one-line system prompt. Gets no kept-tail note: it holds only the doomed span. If it fails,
260
364
  * pi's own compaction serves. Exported for the unit test.
261
365
  */
262
366
  export function standaloneRequest(model: Model<Api>, preparation: Preparation, instruction: string, maxTokens: number): Context {
263
- const messages = [...withoutUsage(convertToLlm(reconstructFromEntries(preparation))), instructionMessage(instruction)];
367
+ const span = withoutThinking(withoutUsage(convertToLlm(reconstructFromEntries(preparation))), model.api);
368
+ const messages = [...span, instructionMessage(instruction)];
264
369
  return fitToBudget(model, { systemPrompt: STANDALONE_SYSTEM_PROMPT, messages }, maxTokens).request;
265
370
  }
266
371
 
267
372
  const instructionMessage = (instruction: string): Message => ({ role: "user", content: instruction, timestamp: Date.now() });
268
373
 
374
+ /**
375
+ * APIs whose request puts a run of consecutive tool results into ONE user
376
+ * message (pi-ai's converters: Anthropic and Bedrock require it, Gemini merges
377
+ * function responses into one turn). The model counts messages on the wire, so
378
+ * the kept-tail note counts such a run once.
379
+ */
380
+ const MERGES_TOOL_RESULTS = new Set(["anthropic-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"]);
381
+ const GOOGLE_APIS = new Set(["google-generative-ai", "google-vertex"]);
382
+
383
+ type TailMessage = { role: string; content?: unknown; toolName?: unknown };
384
+
269
385
  /**
270
386
  * The verbatim-kept tail of the captured request: everything after the doomed
271
387
  * span. pi builds the context as `[compactionSummary?] + one message per entry`
@@ -273,37 +389,64 @@ const instructionMessage = (instruction: string): Message => ({ role: "user", co
273
389
  * `messagesToSummarize`/`turnPrefixMessages` with the same mapping, so the
274
390
  * doomed span is a prefix of the captured array. Alignment is checked role by
275
391
  * role; on any mismatch (an extension that inserted a message) no note is made
276
- * rather than a wrong one. Exported for the unit test.
392
+ * rather than a wrong one.
393
+ *
394
+ * `count` is in the provider's messages (`api`): a parallel batch's results
395
+ * are one message on Anthropic. `landmark` names the first kept message so the
396
+ * model need not count: how its text opens, else the tool it calls, else the
397
+ * tool whose result it is. Exported for the unit test.
277
398
  */
278
399
  export function keptTailOf(
279
- captured: readonly { role: string; content?: unknown }[],
400
+ captured: readonly TailMessage[],
280
401
  preparation: Preparation,
281
- ): { count: number; opening?: string } | undefined {
282
- const doomed: { role: string }[] = [
283
- ...(preparation.previousSummary ? [{ role: "compactionSummary" }] : []),
284
- ...preparation.messagesToSummarize,
285
- ...(preparation.isSplitTurn ? preparation.turnPrefixMessages : []),
286
- ];
402
+ api?: string,
403
+ ): { count: number; landmark?: string } | undefined {
404
+ const doomed: { role: string }[] = [...(preparation.previousSummary ? [{ role: "compactionSummary" }] : []), ...doomedMessages(preparation)];
287
405
  if (doomed.length === 0 || doomed.length > captured.length) return undefined;
288
406
  for (let i = 0; i < doomed.length; i++) {
289
407
  if (captured[i].role !== doomed[i].role) return undefined;
290
408
  }
291
- const count = captured.length - doomed.length;
292
- if (count === 0) return { count };
293
- return { count, opening: openingText(captured[doomed.length]) };
409
+ const tail = captured.slice(doomed.length);
410
+ if (tail.length === 0) return { count: 0 };
411
+ // On Google a tool result's image can become a user message of its own,
412
+ // depending on the model: the count cannot be known, so no note.
413
+ const imageResult = tail.some((m) => m.role === "toolResult" && Array.isArray(m.content) && (m.content as { type?: string }[]).some((b) => b?.type === "image"));
414
+ if (imageResult && api !== undefined && GOOGLE_APIS.has(api)) return undefined;
415
+ const merges = api !== undefined && MERGES_TOOL_RESULTS.has(api);
416
+ const count = tail.filter((m, i) => !(merges && m.role === "toolResult" && tail[i - 1]?.role === "toolResult")).length;
417
+ return { count, landmark: landmarkOf(tail[0]) };
418
+ }
419
+
420
+ /** How the note points at the first kept message, or undefined when nothing identifies it. */
421
+ function landmarkOf(message: TailMessage): string | undefined {
422
+ if (message.role === "toolResult" && typeof message.toolName === "string") return `the result of the ${message.toolName} call`;
423
+ const content = Array.isArray(message.content) ? (message.content as { type?: string; text?: string; name?: unknown; arguments?: unknown }[]) : [];
424
+ const text = typeof message.content === "string" ? message.content : content.find((b) => b?.type === "text" && b.text?.trim())?.text;
425
+ const opening = oneLine(text);
426
+ if (opening) return `the message that opens "${opening}"`;
427
+ const call = content.find((b) => b?.type === "toolCall" && typeof b.name === "string");
428
+ if (message.role === "assistant" && call) {
429
+ const hint = oneLine(callHint(call.arguments));
430
+ return `the assistant message that calls ${call.name as string}${hint ? ` (${hint})` : ""}`;
431
+ }
432
+ return undefined;
433
+ }
434
+
435
+ /** The argument that says what a call is about: its path, command or pattern, else its first string argument. */
436
+ function callHint(args: unknown): string | undefined {
437
+ if (!args || typeof args !== "object") return undefined;
438
+ const record = args as Record<string, unknown>;
439
+ for (const key of ["path", "file_path", "command", "pattern", "url", "query"]) {
440
+ if (typeof record[key] === "string") return record[key] as string;
441
+ }
442
+ return Object.values(record).find((value): value is string => typeof value === "string");
294
443
  }
295
444
 
296
- /** The first ~80 characters of a message's text, single-line, for the landmark quote. */
297
- function openingText(message: { content?: unknown }): string | undefined {
298
- const content = message.content;
299
- const text =
300
- typeof content === "string"
301
- ? content
302
- : Array.isArray(content)
303
- ? (content.find((b) => b && typeof b === "object" && (b as { type?: string }).type === "text") as { text?: string } | undefined)?.text
304
- : undefined;
445
+ /** The first ~80 characters of a text, single-line, for a landmark. */
446
+ function oneLine(text: string | undefined): string | undefined {
305
447
  if (!text) return undefined;
306
448
  const line = text.replace(/\s+/g, " ").trim();
449
+ if (!line) return undefined;
307
450
  return line.length > 80 ? `${line.slice(0, 80)}…` : line;
308
451
  }
309
452
 
@@ -319,9 +462,7 @@ function openingText(message: { content?: unknown }): string | undefined {
319
462
  * summarized with the rest.
320
463
  */
321
464
  function reconstructFromEntries(preparation: Preparation): AgentMessage[] {
322
- const doomed = preparation.isSplitTurn
323
- ? [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages]
324
- : preparation.messagesToSummarize;
465
+ const doomed = doomedMessages(preparation);
325
466
  return preparation.previousSummary
326
467
  ? [
327
468
  {