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