@toolkit-cli/toolkode 1.3.7

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 (676) hide show
  1. package/AGENTS.md +69 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/opencode +179 -0
  6. package/bin/toolkode +17 -0
  7. package/bin/toolkode.cjs +190 -0
  8. package/bunfig.toml +7 -0
  9. package/drizzle.config.ts +10 -0
  10. package/git +0 -0
  11. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  12. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  13. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  14. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  15. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  16. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  17. package/migration/20260225215848_workspace/migration.sql +7 -0
  18. package/migration/20260225215848_workspace/snapshot.json +959 -0
  19. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  20. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  21. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  22. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  23. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  24. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  25. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  26. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  27. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  28. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  29. package/migration/20260323234822_events/migration.sql +13 -0
  30. package/migration/20260323234822_events/snapshot.json +1271 -0
  31. package/package.json +160 -0
  32. package/parsers-config.ts +290 -0
  33. package/script/build-node.ts +54 -0
  34. package/script/build.ts +276 -0
  35. package/script/check-migrations.ts +16 -0
  36. package/script/postinstall.mjs +131 -0
  37. package/script/publish.ts +181 -0
  38. package/script/schema.ts +63 -0
  39. package/script/seed-e2e.ts +60 -0
  40. package/script/upgrade-opentui.ts +64 -0
  41. package/specs/effect-migration.md +293 -0
  42. package/specs/tui-plugins.md +389 -0
  43. package/src/account/account.sql.ts +39 -0
  44. package/src/account/index.ts +397 -0
  45. package/src/account/repo.ts +163 -0
  46. package/src/account/schema.ts +91 -0
  47. package/src/acp/README.md +174 -0
  48. package/src/acp/agent.ts +1743 -0
  49. package/src/acp/session.ts +116 -0
  50. package/src/acp/types.ts +24 -0
  51. package/src/agent/agent.ts +418 -0
  52. package/src/agent/generate.txt +75 -0
  53. package/src/agent/prompt/compaction.txt +14 -0
  54. package/src/agent/prompt/explore.txt +18 -0
  55. package/src/agent/prompt/summary.txt +11 -0
  56. package/src/agent/prompt/title.txt +44 -0
  57. package/src/auth/index.ts +115 -0
  58. package/src/bun/index.ts +128 -0
  59. package/src/bun/registry.ts +50 -0
  60. package/src/bus/bus-event.ts +40 -0
  61. package/src/bus/global.ts +10 -0
  62. package/src/bus/index.ts +184 -0
  63. package/src/channel/index.ts +231 -0
  64. package/src/cli/bootstrap.ts +17 -0
  65. package/src/cli/cmd/account.ts +257 -0
  66. package/src/cli/cmd/acp.ts +70 -0
  67. package/src/cli/cmd/agent.ts +245 -0
  68. package/src/cli/cmd/cmd.ts +7 -0
  69. package/src/cli/cmd/db.ts +119 -0
  70. package/src/cli/cmd/debug/agent.ts +167 -0
  71. package/src/cli/cmd/debug/config.ts +16 -0
  72. package/src/cli/cmd/debug/file.ts +97 -0
  73. package/src/cli/cmd/debug/index.ts +48 -0
  74. package/src/cli/cmd/debug/lsp.ts +53 -0
  75. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  76. package/src/cli/cmd/debug/scrap.ts +16 -0
  77. package/src/cli/cmd/debug/skill.ts +16 -0
  78. package/src/cli/cmd/debug/snapshot.ts +52 -0
  79. package/src/cli/cmd/export.ts +89 -0
  80. package/src/cli/cmd/generate.ts +38 -0
  81. package/src/cli/cmd/github.ts +1646 -0
  82. package/src/cli/cmd/import.ts +207 -0
  83. package/src/cli/cmd/mcp.ts +754 -0
  84. package/src/cli/cmd/models.ts +78 -0
  85. package/src/cli/cmd/plug.ts +231 -0
  86. package/src/cli/cmd/pr.ts +127 -0
  87. package/src/cli/cmd/providers.ts +482 -0
  88. package/src/cli/cmd/run.ts +738 -0
  89. package/src/cli/cmd/serve.ts +42 -0
  90. package/src/cli/cmd/session.ts +159 -0
  91. package/src/cli/cmd/stats.ts +410 -0
  92. package/src/cli/cmd/tui/app.tsx +1255 -0
  93. package/src/cli/cmd/tui/attach.ts +88 -0
  94. package/src/cli/cmd/tui/component/border.tsx +21 -0
  95. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  96. package/src/cli/cmd/tui/component/dialog-command.tsx +171 -0
  97. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  98. package/src/cli/cmd/tui/component/dialog-model.tsx +264 -0
  99. package/src/cli/cmd/tui/component/dialog-provider.tsx +334 -0
  100. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  101. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  102. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  103. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  104. package/src/cli/cmd/tui/component/dialog-status.tsx +168 -0
  105. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  106. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  107. package/src/cli/cmd/tui/component/dialog-variant.tsx +29 -0
  108. package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +320 -0
  109. package/src/cli/cmd/tui/component/error-component.tsx +91 -0
  110. package/src/cli/cmd/tui/component/logo.tsx +86 -0
  111. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  112. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
  113. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  114. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  115. package/src/cli/cmd/tui/component/prompt/index.tsx +1353 -0
  116. package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
  117. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  118. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  119. package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
  120. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  121. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  122. package/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx +151 -0
  123. package/src/cli/cmd/tui/context/args.tsx +15 -0
  124. package/src/cli/cmd/tui/context/directory.ts +13 -0
  125. package/src/cli/cmd/tui/context/exit.tsx +60 -0
  126. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  127. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  128. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  129. package/src/cli/cmd/tui/context/local.tsx +406 -0
  130. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  131. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  132. package/src/cli/cmd/tui/context/route.tsx +52 -0
  133. package/src/cli/cmd/tui/context/sdk.tsx +128 -0
  134. package/src/cli/cmd/tui/context/sync.tsx +504 -0
  135. package/src/cli/cmd/tui/context/theme/amber.json +245 -0
  136. package/src/cli/cmd/tui/context/theme/amiga.json +245 -0
  137. package/src/cli/cmd/tui/context/theme/atari.json +245 -0
  138. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  139. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  140. package/src/cli/cmd/tui/context/theme/borland.json +245 -0
  141. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  142. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  143. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  144. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  145. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  146. package/src/cli/cmd/tui/context/theme/commodore.json +245 -0
  147. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  148. package/src/cli/cmd/tui/context/theme/dos-edit.json +245 -0
  149. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  150. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  151. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  152. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  153. package/src/cli/cmd/tui/context/theme/gnu.json +245 -0
  154. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  155. package/src/cli/cmd/tui/context/theme/hacker.json +245 -0
  156. package/src/cli/cmd/tui/context/theme/irix.json +245 -0
  157. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  158. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  159. package/src/cli/cmd/tui/context/theme/mac84.json +245 -0
  160. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  161. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  162. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  163. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  164. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  165. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  166. package/src/cli/cmd/tui/context/theme/norton.json +245 -0
  167. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  168. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  169. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  170. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  171. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  172. package/src/cli/cmd/tui/context/theme/pine.json +245 -0
  173. package/src/cli/cmd/tui/context/theme/retrowave.json +245 -0
  174. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  175. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  176. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  177. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  178. package/src/cli/cmd/tui/context/theme/toolkode.json +245 -0
  179. package/src/cli/cmd/tui/context/theme/tron.json +245 -0
  180. package/src/cli/cmd/tui/context/theme/ubuntu.json +245 -0
  181. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  182. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  183. package/src/cli/cmd/tui/context/theme/vt100.json +245 -0
  184. package/src/cli/cmd/tui/context/theme/xcode.json +245 -0
  185. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  186. package/src/cli/cmd/tui/context/theme.tsx +1288 -0
  187. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  188. package/src/cli/cmd/tui/event.ts +49 -0
  189. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +152 -0
  190. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
  191. package/src/cli/cmd/tui/feature-plugins/sidebar/agents-panel.tsx +95 -0
  192. package/src/cli/cmd/tui/feature-plugins/sidebar/btw-panel.tsx +105 -0
  193. package/src/cli/cmd/tui/feature-plugins/sidebar/commands-panel.tsx +40 -0
  194. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -0
  195. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  196. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  197. package/src/cli/cmd/tui/feature-plugins/sidebar/git-panel.tsx +36 -0
  198. package/src/cli/cmd/tui/feature-plugins/sidebar/loop-panel.tsx +124 -0
  199. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  200. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
  201. package/src/cli/cmd/tui/feature-plugins/sidebar/session-panel.tsx +48 -0
  202. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
  203. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +270 -0
  204. package/src/cli/cmd/tui/plugin/api.tsx +420 -0
  205. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  206. package/src/cli/cmd/tui/plugin/internal.ts +37 -0
  207. package/src/cli/cmd/tui/plugin/runtime.ts +967 -0
  208. package/src/cli/cmd/tui/plugin/slots.tsx +61 -0
  209. package/src/cli/cmd/tui/routes/home.tsx +173 -0
  210. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
  211. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
  212. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  213. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  214. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  215. package/src/cli/cmd/tui/routes/session/index.tsx +2229 -0
  216. package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
  217. package/src/cli/cmd/tui/routes/session/question.tsx +467 -0
  218. package/src/cli/cmd/tui/routes/session/sidebar.tsx +72 -0
  219. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +131 -0
  220. package/src/cli/cmd/tui/thread.ts +232 -0
  221. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  222. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +89 -0
  223. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +208 -0
  224. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  225. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +106 -0
  226. package/src/cli/cmd/tui/ui/dialog-select.tsx +402 -0
  227. package/src/cli/cmd/tui/ui/dialog.tsx +192 -0
  228. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  229. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  230. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  231. package/src/cli/cmd/tui/util/clipboard.ts +192 -0
  232. package/src/cli/cmd/tui/util/editor.ts +37 -0
  233. package/src/cli/cmd/tui/util/selection.ts +25 -0
  234. package/src/cli/cmd/tui/util/signal.ts +7 -0
  235. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  236. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  237. package/src/cli/cmd/tui/win32.ts +129 -0
  238. package/src/cli/cmd/tui/worker.ts +204 -0
  239. package/src/cli/cmd/uninstall.ts +353 -0
  240. package/src/cli/cmd/upgrade.ts +73 -0
  241. package/src/cli/cmd/web.ts +81 -0
  242. package/src/cli/effect/prompt.ts +25 -0
  243. package/src/cli/error.ts +46 -0
  244. package/src/cli/logo.ts +7 -0
  245. package/src/cli/network.ts +60 -0
  246. package/src/cli/ui.ts +116 -0
  247. package/src/cli/upgrade.ts +31 -0
  248. package/src/command/index.ts +195 -0
  249. package/src/command/template/initialize.txt +10 -0
  250. package/src/command/template/review.txt +101 -0
  251. package/src/config/config.ts +1693 -0
  252. package/src/config/markdown.ts +99 -0
  253. package/src/config/migrate-tui-config.ts +155 -0
  254. package/src/config/paths.ts +174 -0
  255. package/src/config/tui-schema.ts +36 -0
  256. package/src/config/tui.ts +212 -0
  257. package/src/control-plane/adaptors/index.ts +20 -0
  258. package/src/control-plane/adaptors/worktree.ts +38 -0
  259. package/src/control-plane/schema.ts +17 -0
  260. package/src/control-plane/sse.ts +66 -0
  261. package/src/control-plane/types.ts +21 -0
  262. package/src/control-plane/workspace.sql.ts +17 -0
  263. package/src/control-plane/workspace.ts +154 -0
  264. package/src/cron/index.ts +241 -0
  265. package/src/cron/parse.ts +189 -0
  266. package/src/effect/cross-spawn-spawner.ts +479 -0
  267. package/src/effect/instance-registry.ts +12 -0
  268. package/src/effect/instance-state.ts +47 -0
  269. package/src/effect/run-service.ts +19 -0
  270. package/src/env/index.ts +28 -0
  271. package/src/file/ignore.ts +82 -0
  272. package/src/file/index.ts +693 -0
  273. package/src/file/protected.ts +59 -0
  274. package/src/file/ripgrep.ts +376 -0
  275. package/src/file/time.ts +128 -0
  276. package/src/file/watcher.ts +171 -0
  277. package/src/filesystem/index.ts +226 -0
  278. package/src/flag/flag.ts +157 -0
  279. package/src/format/formatter.ts +396 -0
  280. package/src/format/index.ts +199 -0
  281. package/src/global/index.ts +54 -0
  282. package/src/hooks/index.ts +302 -0
  283. package/src/id/id.ts +85 -0
  284. package/src/ide/index.ts +74 -0
  285. package/src/index.ts +243 -0
  286. package/src/installation/index.ts +363 -0
  287. package/src/lsp/client.ts +252 -0
  288. package/src/lsp/index.ts +558 -0
  289. package/src/lsp/language.ts +120 -0
  290. package/src/lsp/launch.ts +21 -0
  291. package/src/lsp/server.ts +2093 -0
  292. package/src/mcp/auth.ts +181 -0
  293. package/src/mcp/index.ts +926 -0
  294. package/src/mcp/oauth-callback.ts +215 -0
  295. package/src/mcp/oauth-provider.ts +185 -0
  296. package/src/node.ts +1 -0
  297. package/src/patch/index.ts +680 -0
  298. package/src/permission/arity.ts +163 -0
  299. package/src/permission/evaluate.ts +15 -0
  300. package/src/permission/index.ts +322 -0
  301. package/src/permission/schema.ts +17 -0
  302. package/src/plugin/codex.ts +628 -0
  303. package/src/plugin/copilot.ts +343 -0
  304. package/src/plugin/index.ts +331 -0
  305. package/src/plugin/install.ts +384 -0
  306. package/src/plugin/meta.ts +165 -0
  307. package/src/plugin/shared.ts +172 -0
  308. package/src/project/bootstrap.ts +31 -0
  309. package/src/project/instance.ts +167 -0
  310. package/src/project/project.sql.ts +16 -0
  311. package/src/project/project.ts +519 -0
  312. package/src/project/schema.ts +16 -0
  313. package/src/project/state.ts +70 -0
  314. package/src/project/vcs.ts +124 -0
  315. package/src/provider/auth.ts +252 -0
  316. package/src/provider/error.ts +197 -0
  317. package/src/provider/models.ts +138 -0
  318. package/src/provider/provider.ts +1593 -0
  319. package/src/provider/schema.ts +39 -0
  320. package/src/provider/sdk/copilot/README.md +5 -0
  321. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  322. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  323. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  324. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  325. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  326. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  327. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  328. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  329. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  330. package/src/provider/sdk/copilot/index.ts +2 -0
  331. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  332. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  333. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  334. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  335. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  336. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  337. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1769 -0
  338. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  339. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  340. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  341. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  342. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  343. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  344. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  345. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  346. package/src/provider/toolkit-manifest.ts +110 -0
  347. package/src/provider/transform.ts +1045 -0
  348. package/src/pty/index.ts +397 -0
  349. package/src/pty/schema.ts +17 -0
  350. package/src/question/index.ts +221 -0
  351. package/src/question/schema.ts +17 -0
  352. package/src/server/error.ts +36 -0
  353. package/src/server/event.ts +7 -0
  354. package/src/server/instance.ts +285 -0
  355. package/src/server/mdns.ts +60 -0
  356. package/src/server/middleware.ts +29 -0
  357. package/src/server/projectors.ts +28 -0
  358. package/src/server/router.ts +99 -0
  359. package/src/server/routes/config.ts +92 -0
  360. package/src/server/routes/event.ts +83 -0
  361. package/src/server/routes/experimental.ts +271 -0
  362. package/src/server/routes/file.ts +197 -0
  363. package/src/server/routes/global.ts +339 -0
  364. package/src/server/routes/mcp.ts +225 -0
  365. package/src/server/routes/permission.ts +69 -0
  366. package/src/server/routes/project.ts +118 -0
  367. package/src/server/routes/provider.ts +171 -0
  368. package/src/server/routes/pty.ts +211 -0
  369. package/src/server/routes/question.ts +99 -0
  370. package/src/server/routes/session.ts +1031 -0
  371. package/src/server/routes/tui.ts +379 -0
  372. package/src/server/routes/workspace.ts +94 -0
  373. package/src/server/server.ts +312 -0
  374. package/src/session/compaction.ts +424 -0
  375. package/src/session/index.ts +882 -0
  376. package/src/session/instruction.ts +321 -0
  377. package/src/session/llm.ts +341 -0
  378. package/src/session/message-v2.ts +1030 -0
  379. package/src/session/message.ts +191 -0
  380. package/src/session/overflow.ts +22 -0
  381. package/src/session/processor.ts +554 -0
  382. package/src/session/projectors.ts +135 -0
  383. package/src/session/prompt/anthropic.txt +105 -0
  384. package/src/session/prompt/beast.txt +147 -0
  385. package/src/session/prompt/build-switch.txt +5 -0
  386. package/src/session/prompt/codex.txt +79 -0
  387. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  388. package/src/session/prompt/default.txt +108 -0
  389. package/src/session/prompt/gemini.txt +155 -0
  390. package/src/session/prompt/gpt.txt +107 -0
  391. package/src/session/prompt/max-steps.txt +16 -0
  392. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  393. package/src/session/prompt/plan.txt +26 -0
  394. package/src/session/prompt/trinity.txt +97 -0
  395. package/src/session/prompt.ts +2058 -0
  396. package/src/session/retry.ts +106 -0
  397. package/src/session/revert.ts +138 -0
  398. package/src/session/schema.ts +38 -0
  399. package/src/session/session.sql.ts +103 -0
  400. package/src/session/status.ts +102 -0
  401. package/src/session/summary.ts +170 -0
  402. package/src/session/system.ts +74 -0
  403. package/src/session/todo.ts +57 -0
  404. package/src/share/share-next.ts +288 -0
  405. package/src/share/share.sql.ts +13 -0
  406. package/src/shell/shell.ts +73 -0
  407. package/src/skill/discovery.ts +116 -0
  408. package/src/skill/index.ts +284 -0
  409. package/src/skills-marketplace/index.ts +305 -0
  410. package/src/snapshot/index.ts +489 -0
  411. package/src/sql.d.ts +4 -0
  412. package/src/storage/db.bun.ts +8 -0
  413. package/src/storage/db.node.ts +8 -0
  414. package/src/storage/db.ts +177 -0
  415. package/src/storage/json-migration.ts +425 -0
  416. package/src/storage/schema.sql.ts +10 -0
  417. package/src/storage/schema.ts +5 -0
  418. package/src/storage/storage.ts +217 -0
  419. package/src/sync/README.md +179 -0
  420. package/src/sync/event.sql.ts +16 -0
  421. package/src/sync/index.ts +263 -0
  422. package/src/sync/schema.ts +14 -0
  423. package/src/team/index.ts +428 -0
  424. package/src/tool/apply_patch.ts +281 -0
  425. package/src/tool/apply_patch.txt +33 -0
  426. package/src/tool/bash.ts +271 -0
  427. package/src/tool/bash.txt +115 -0
  428. package/src/tool/batch.ts +183 -0
  429. package/src/tool/batch.txt +24 -0
  430. package/src/tool/codesearch.ts +132 -0
  431. package/src/tool/codesearch.txt +12 -0
  432. package/src/tool/cron-create.ts +54 -0
  433. package/src/tool/cron-create.txt +16 -0
  434. package/src/tool/cron-delete.ts +29 -0
  435. package/src/tool/cron-delete.txt +1 -0
  436. package/src/tool/cron-list.ts +41 -0
  437. package/src/tool/cron-list.txt +1 -0
  438. package/src/tool/edit.ts +667 -0
  439. package/src/tool/edit.txt +10 -0
  440. package/src/tool/external-directory.ts +32 -0
  441. package/src/tool/glob.ts +78 -0
  442. package/src/tool/glob.txt +6 -0
  443. package/src/tool/grep.ts +156 -0
  444. package/src/tool/grep.txt +8 -0
  445. package/src/tool/invalid.ts +17 -0
  446. package/src/tool/ls.ts +121 -0
  447. package/src/tool/ls.txt +1 -0
  448. package/src/tool/lsp.ts +97 -0
  449. package/src/tool/lsp.txt +19 -0
  450. package/src/tool/multiedit.ts +46 -0
  451. package/src/tool/multiedit.txt +41 -0
  452. package/src/tool/plan-enter.txt +14 -0
  453. package/src/tool/plan-exit.txt +13 -0
  454. package/src/tool/plan.ts +131 -0
  455. package/src/tool/question.ts +33 -0
  456. package/src/tool/question.txt +10 -0
  457. package/src/tool/read.ts +293 -0
  458. package/src/tool/read.txt +14 -0
  459. package/src/tool/registry.ts +232 -0
  460. package/src/tool/schema.ts +17 -0
  461. package/src/tool/send-message.ts +59 -0
  462. package/src/tool/send-message.txt +7 -0
  463. package/src/tool/skill.ts +105 -0
  464. package/src/tool/task.ts +230 -0
  465. package/src/tool/task.txt +62 -0
  466. package/src/tool/team.ts +235 -0
  467. package/src/tool/team.txt +22 -0
  468. package/src/tool/todo.ts +31 -0
  469. package/src/tool/todowrite.txt +167 -0
  470. package/src/tool/tool.ts +90 -0
  471. package/src/tool/truncate.ts +144 -0
  472. package/src/tool/truncation-dir.ts +4 -0
  473. package/src/tool/webfetch.ts +206 -0
  474. package/src/tool/webfetch.txt +13 -0
  475. package/src/tool/websearch.ts +150 -0
  476. package/src/tool/websearch.txt +14 -0
  477. package/src/tool/write.ts +84 -0
  478. package/src/tool/write.txt +8 -0
  479. package/src/util/abort.ts +35 -0
  480. package/src/util/archive.ts +17 -0
  481. package/src/util/color.ts +19 -0
  482. package/src/util/context.ts +25 -0
  483. package/src/util/data-url.ts +9 -0
  484. package/src/util/defer.ts +12 -0
  485. package/src/util/effect-http-client.ts +11 -0
  486. package/src/util/effect-zod.ts +98 -0
  487. package/src/util/error.ts +77 -0
  488. package/src/util/filesystem.ts +203 -0
  489. package/src/util/flock.ts +333 -0
  490. package/src/util/fn.ts +21 -0
  491. package/src/util/format.ts +20 -0
  492. package/src/util/git.ts +35 -0
  493. package/src/util/glob.ts +34 -0
  494. package/src/util/hash.ts +7 -0
  495. package/src/util/iife.ts +3 -0
  496. package/src/util/keybind.ts +103 -0
  497. package/src/util/lazy.ts +23 -0
  498. package/src/util/locale.ts +81 -0
  499. package/src/util/lock.ts +98 -0
  500. package/src/util/log.ts +182 -0
  501. package/src/util/network.ts +9 -0
  502. package/src/util/process.ts +172 -0
  503. package/src/util/queue.ts +32 -0
  504. package/src/util/record.ts +3 -0
  505. package/src/util/rpc.ts +66 -0
  506. package/src/util/schema.ts +53 -0
  507. package/src/util/scrap.ts +10 -0
  508. package/src/util/signal.ts +12 -0
  509. package/src/util/timeout.ts +14 -0
  510. package/src/util/token.ts +7 -0
  511. package/src/util/update-schema.ts +13 -0
  512. package/src/util/which.ts +14 -0
  513. package/src/util/wildcard.ts +59 -0
  514. package/src/worktree/index.ts +638 -0
  515. package/sst-env.d.ts +10 -0
  516. package/test/AGENTS.md +81 -0
  517. package/test/account/repo.test.ts +326 -0
  518. package/test/account/service.test.ts +282 -0
  519. package/test/acp/agent-interface.test.ts +51 -0
  520. package/test/acp/event-subscription.test.ts +685 -0
  521. package/test/agent/agent.test.ts +717 -0
  522. package/test/auth/auth.test.ts +58 -0
  523. package/test/bun.test.ts +53 -0
  524. package/test/bus/bus-effect.test.ts +164 -0
  525. package/test/bus/bus-integration.test.ts +87 -0
  526. package/test/bus/bus.test.ts +219 -0
  527. package/test/cli/account.test.ts +26 -0
  528. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  529. package/test/cli/github-action.test.ts +198 -0
  530. package/test/cli/github-remote.test.ts +80 -0
  531. package/test/cli/import.test.ts +54 -0
  532. package/test/cli/plugin-auth-picker.test.ts +120 -0
  533. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  534. package/test/cli/tui/plugin-add.test.ts +61 -0
  535. package/test/cli/tui/plugin-install.test.ts +95 -0
  536. package/test/cli/tui/plugin-lifecycle.test.ts +225 -0
  537. package/test/cli/tui/plugin-loader-entrypoint.test.ts +189 -0
  538. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  539. package/test/cli/tui/plugin-loader.test.ts +563 -0
  540. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  541. package/test/cli/tui/theme-store.test.ts +51 -0
  542. package/test/cli/tui/thread.test.ts +128 -0
  543. package/test/cli/tui/transcript.test.ts +322 -0
  544. package/test/config/agent-color.test.ts +71 -0
  545. package/test/config/config.test.ts +2187 -0
  546. package/test/config/fixtures/empty-frontmatter.md +4 -0
  547. package/test/config/fixtures/frontmatter.md +28 -0
  548. package/test/config/fixtures/markdown-header.md +11 -0
  549. package/test/config/fixtures/no-frontmatter.md +1 -0
  550. package/test/config/fixtures/weird-model-id.md +13 -0
  551. package/test/config/markdown.test.ts +228 -0
  552. package/test/config/tui.test.ts +667 -0
  553. package/test/control-plane/sse.test.ts +56 -0
  554. package/test/effect/cross-spawn-spawner.test.ts +402 -0
  555. package/test/effect/instance-state.test.ts +384 -0
  556. package/test/effect/run-service.test.ts +46 -0
  557. package/test/file/fsmonitor.test.ts +62 -0
  558. package/test/file/ignore.test.ts +10 -0
  559. package/test/file/index.test.ts +946 -0
  560. package/test/file/path-traversal.test.ts +198 -0
  561. package/test/file/ripgrep.test.ts +54 -0
  562. package/test/file/time.test.ts +354 -0
  563. package/test/file/watcher.test.ts +247 -0
  564. package/test/filesystem/filesystem.test.ts +319 -0
  565. package/test/fixture/db.ts +11 -0
  566. package/test/fixture/fixture.test.ts +26 -0
  567. package/test/fixture/fixture.ts +141 -0
  568. package/test/fixture/flock-worker.ts +72 -0
  569. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  570. package/test/fixture/plug-worker.ts +93 -0
  571. package/test/fixture/plugin-meta-worker.ts +26 -0
  572. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  573. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  574. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  575. package/test/fixture/skills/index.json +6 -0
  576. package/test/fixture/tui-plugin.ts +335 -0
  577. package/test/fixture/tui-runtime.ts +34 -0
  578. package/test/format/format.test.ts +179 -0
  579. package/test/ide/ide.test.ts +82 -0
  580. package/test/installation/installation.test.ts +151 -0
  581. package/test/keybind.test.ts +421 -0
  582. package/test/lib/effect.ts +37 -0
  583. package/test/lib/filesystem.ts +10 -0
  584. package/test/lsp/client.test.ts +95 -0
  585. package/test/lsp/index.test.ts +55 -0
  586. package/test/lsp/launch.test.ts +22 -0
  587. package/test/lsp/lifecycle.test.ts +147 -0
  588. package/test/mcp/headers.test.ts +153 -0
  589. package/test/mcp/lifecycle.test.ts +750 -0
  590. package/test/mcp/oauth-auto-connect.test.ts +199 -0
  591. package/test/mcp/oauth-browser.test.ts +249 -0
  592. package/test/memory/abort-leak.test.ts +137 -0
  593. package/test/patch/patch.test.ts +348 -0
  594. package/test/permission/arity.test.ts +33 -0
  595. package/test/permission/next.test.ts +1148 -0
  596. package/test/permission-task.test.ts +323 -0
  597. package/test/plugin/auth-override.test.ts +74 -0
  598. package/test/plugin/codex.test.ts +123 -0
  599. package/test/plugin/install-concurrency.test.ts +134 -0
  600. package/test/plugin/install.test.ts +504 -0
  601. package/test/plugin/loader-shared.test.ts +625 -0
  602. package/test/plugin/meta.test.ts +137 -0
  603. package/test/plugin/trigger.test.ts +111 -0
  604. package/test/preload.ts +90 -0
  605. package/test/project/migrate-global.test.ts +140 -0
  606. package/test/project/project.test.ts +459 -0
  607. package/test/project/state.test.ts +115 -0
  608. package/test/project/vcs.test.ts +116 -0
  609. package/test/project/worktree-remove.test.ts +96 -0
  610. package/test/project/worktree.test.ts +173 -0
  611. package/test/provider/amazon-bedrock.test.ts +447 -0
  612. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  613. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  614. package/test/provider/gitlab-duo.test.ts +412 -0
  615. package/test/provider/provider.test.ts +2284 -0
  616. package/test/provider/transform.test.ts +2758 -0
  617. package/test/pty/pty-output-isolation.test.ts +141 -0
  618. package/test/pty/pty-session.test.ts +92 -0
  619. package/test/question/question.test.ts +453 -0
  620. package/test/server/global-session-list.test.ts +89 -0
  621. package/test/server/project-init-git.test.ts +121 -0
  622. package/test/server/session-list.test.ts +90 -0
  623. package/test/server/session-messages.test.ts +132 -0
  624. package/test/server/session-select.test.ts +78 -0
  625. package/test/session/compaction.test.ts +1094 -0
  626. package/test/session/instruction.test.ts +170 -0
  627. package/test/session/llm.test.ts +882 -0
  628. package/test/session/message-v2.test.ts +957 -0
  629. package/test/session/messages-pagination.test.ts +115 -0
  630. package/test/session/processor-effect.test.ts +838 -0
  631. package/test/session/prompt.test.ts +518 -0
  632. package/test/session/retry.test.ts +232 -0
  633. package/test/session/revert-compact.test.ts +286 -0
  634. package/test/session/session.test.ts +142 -0
  635. package/test/session/structured-output-integration.test.ts +233 -0
  636. package/test/session/structured-output.test.ts +391 -0
  637. package/test/session/system.test.ts +59 -0
  638. package/test/share/share-next.test.ts +76 -0
  639. package/test/skill/discovery.test.ts +116 -0
  640. package/test/skill/skill.test.ts +392 -0
  641. package/test/snapshot/snapshot.test.ts +1235 -0
  642. package/test/storage/db.test.ts +14 -0
  643. package/test/storage/json-migration.test.ts +849 -0
  644. package/test/sync/index.test.ts +191 -0
  645. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  646. package/test/tool/apply_patch.test.ts +567 -0
  647. package/test/tool/bash.test.ts +403 -0
  648. package/test/tool/edit.test.ts +681 -0
  649. package/test/tool/external-directory.test.ts +128 -0
  650. package/test/tool/fixtures/large-image.png +0 -0
  651. package/test/tool/fixtures/models-api.json +38413 -0
  652. package/test/tool/grep.test.ts +111 -0
  653. package/test/tool/question.test.ts +108 -0
  654. package/test/tool/read.test.ts +509 -0
  655. package/test/tool/registry.test.ts +126 -0
  656. package/test/tool/skill.test.ts +167 -0
  657. package/test/tool/task.test.ts +49 -0
  658. package/test/tool/truncation.test.ts +161 -0
  659. package/test/tool/webfetch.test.ts +101 -0
  660. package/test/tool/write.test.ts +353 -0
  661. package/test/util/data-url.test.ts +14 -0
  662. package/test/util/effect-zod.test.ts +61 -0
  663. package/test/util/error.test.ts +38 -0
  664. package/test/util/filesystem.test.ts +558 -0
  665. package/test/util/flock.test.ts +383 -0
  666. package/test/util/format.test.ts +59 -0
  667. package/test/util/glob.test.ts +164 -0
  668. package/test/util/iife.test.ts +36 -0
  669. package/test/util/lazy.test.ts +50 -0
  670. package/test/util/lock.test.ts +72 -0
  671. package/test/util/module.test.ts +59 -0
  672. package/test/util/process.test.ts +128 -0
  673. package/test/util/timeout.test.ts +21 -0
  674. package/test/util/which.test.ts +100 -0
  675. package/test/util/wildcard.test.ts +90 -0
  676. package/tsconfig.json +23 -0
@@ -0,0 +1,1094 @@
1
+ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
2
+ import { APICallError } from "ai"
3
+ import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect"
4
+ import * as Stream from "effect/Stream"
5
+ import path from "path"
6
+ import { Bus } from "../../src/bus"
7
+ import { Config } from "../../src/config/config"
8
+ import { Agent } from "../../src/agent/agent"
9
+ import { LLM } from "../../src/session/llm"
10
+ import { SessionCompaction } from "../../src/session/compaction"
11
+ import { Token } from "../../src/util/token"
12
+ import { Instance } from "../../src/project/instance"
13
+ import { Log } from "../../src/util/log"
14
+ import { Permission } from "../../src/permission"
15
+ import { Plugin } from "../../src/plugin"
16
+ import { tmpdir } from "../fixture/fixture"
17
+ import { Session } from "../../src/session"
18
+ import { MessageV2 } from "../../src/session/message-v2"
19
+ import { MessageID, PartID, SessionID } from "../../src/session/schema"
20
+ import { SessionStatus } from "../../src/session/status"
21
+ import { ModelID, ProviderID } from "../../src/provider/schema"
22
+ import type { Provider } from "../../src/provider/provider"
23
+ import * as ProviderModule from "../../src/provider/provider"
24
+ import * as SessionProcessorModule from "../../src/session/processor"
25
+ import { Snapshot } from "../../src/snapshot"
26
+
27
+ Log.init({ print: false })
28
+
29
+ const ref = {
30
+ providerID: ProviderID.make("test"),
31
+ modelID: ModelID.make("test-model"),
32
+ }
33
+
34
+ afterEach(() => {
35
+ mock.restore()
36
+ })
37
+
38
+ function createModel(opts: {
39
+ context: number
40
+ output: number
41
+ input?: number
42
+ cost?: Provider.Model["cost"]
43
+ npm?: string
44
+ }): Provider.Model {
45
+ return {
46
+ id: "test-model",
47
+ providerID: "test",
48
+ name: "Test",
49
+ limit: {
50
+ context: opts.context,
51
+ input: opts.input,
52
+ output: opts.output,
53
+ },
54
+ cost: opts.cost ?? { input: 0, output: 0, cache: { read: 0, write: 0 } },
55
+ capabilities: {
56
+ toolcall: true,
57
+ attachment: false,
58
+ reasoning: false,
59
+ temperature: true,
60
+ input: { text: true, image: false, audio: false, video: false },
61
+ output: { text: true, image: false, audio: false, video: false },
62
+ },
63
+ api: { npm: opts.npm ?? "@ai-sdk/anthropic" },
64
+ options: {},
65
+ } as Provider.Model
66
+ }
67
+
68
+ async function user(sessionID: SessionID, text: string) {
69
+ const msg = await Session.updateMessage({
70
+ id: MessageID.ascending(),
71
+ role: "user",
72
+ sessionID,
73
+ agent: "build",
74
+ model: ref,
75
+ time: { created: Date.now() },
76
+ })
77
+ await Session.updatePart({
78
+ id: PartID.ascending(),
79
+ messageID: msg.id,
80
+ sessionID,
81
+ type: "text",
82
+ text,
83
+ })
84
+ return msg
85
+ }
86
+
87
+ async function assistant(sessionID: SessionID, parentID: MessageID, root: string) {
88
+ const msg: MessageV2.Assistant = {
89
+ id: MessageID.ascending(),
90
+ role: "assistant",
91
+ sessionID,
92
+ mode: "build",
93
+ agent: "build",
94
+ path: { cwd: root, root },
95
+ cost: 0,
96
+ tokens: {
97
+ output: 0,
98
+ input: 0,
99
+ reasoning: 0,
100
+ cache: { read: 0, write: 0 },
101
+ },
102
+ modelID: ref.modelID,
103
+ providerID: ref.providerID,
104
+ parentID,
105
+ time: { created: Date.now() },
106
+ finish: "end_turn",
107
+ }
108
+ await Session.updateMessage(msg)
109
+ return msg
110
+ }
111
+
112
+ async function tool(sessionID: SessionID, messageID: MessageID, tool: string, output: string) {
113
+ return Session.updatePart({
114
+ id: PartID.ascending(),
115
+ messageID,
116
+ sessionID,
117
+ type: "tool",
118
+ callID: crypto.randomUUID(),
119
+ tool,
120
+ state: {
121
+ status: "completed",
122
+ input: {},
123
+ output,
124
+ title: "done",
125
+ metadata: {},
126
+ time: { start: Date.now(), end: Date.now() },
127
+ },
128
+ })
129
+ }
130
+
131
+ function fake(
132
+ input: Parameters<(typeof SessionProcessorModule.SessionProcessor)["create"]>[0],
133
+ result: "continue" | "compact",
134
+ ) {
135
+ const msg = input.assistantMessage
136
+ return {
137
+ get message() {
138
+ return msg
139
+ },
140
+ abort: Effect.fn("TestSessionProcessor.abort")(() => Effect.void),
141
+ partFromToolCall() {
142
+ return {
143
+ id: PartID.ascending(),
144
+ messageID: msg.id,
145
+ sessionID: msg.sessionID,
146
+ type: "tool",
147
+ callID: "fake",
148
+ tool: "fake",
149
+ state: { status: "pending", input: {}, raw: "" },
150
+ }
151
+ },
152
+ process: Effect.fn("TestSessionProcessor.process")(() => Effect.succeed(result)),
153
+ } satisfies SessionProcessorModule.SessionProcessor.Handle
154
+ }
155
+
156
+ function layer(result: "continue" | "compact") {
157
+ return Layer.succeed(
158
+ SessionProcessorModule.SessionProcessor.Service,
159
+ SessionProcessorModule.SessionProcessor.Service.of({
160
+ create: Effect.fn("TestSessionProcessor.create")((input) => Effect.succeed(fake(input, result))),
161
+ }),
162
+ )
163
+ }
164
+
165
+ function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer) {
166
+ const bus = Bus.layer
167
+ return ManagedRuntime.make(
168
+ Layer.mergeAll(SessionCompaction.layer, bus).pipe(
169
+ Layer.provide(Session.defaultLayer),
170
+ Layer.provide(layer(result)),
171
+ Layer.provide(Agent.defaultLayer),
172
+ Layer.provide(plugin),
173
+ Layer.provide(bus),
174
+ Layer.provide(Config.defaultLayer),
175
+ ),
176
+ )
177
+ }
178
+
179
+ function llm() {
180
+ const queue: Array<
181
+ Stream.Stream<LLM.Event, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLM.Event, unknown>)
182
+ > = []
183
+
184
+ return {
185
+ push(stream: Stream.Stream<LLM.Event, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLM.Event, unknown>)) {
186
+ queue.push(stream)
187
+ },
188
+ layer: Layer.succeed(
189
+ LLM.Service,
190
+ LLM.Service.of({
191
+ stream: (input) => {
192
+ const item = queue.shift() ?? Stream.empty
193
+ const stream = typeof item === "function" ? item(input) : item
194
+ return stream.pipe(Stream.mapEffect((event) => Effect.succeed(event)))
195
+ },
196
+ }),
197
+ ),
198
+ }
199
+ }
200
+
201
+ function liveRuntime(layer: Layer.Layer<LLM.Service>) {
202
+ const bus = Bus.layer
203
+ const status = SessionStatus.layer.pipe(Layer.provide(bus))
204
+ const processor = SessionProcessorModule.SessionProcessor.layer
205
+ return ManagedRuntime.make(
206
+ Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
207
+ Layer.provide(Session.defaultLayer),
208
+ Layer.provide(Snapshot.defaultLayer),
209
+ Layer.provide(layer),
210
+ Layer.provide(Permission.layer),
211
+ Layer.provide(Agent.defaultLayer),
212
+ Layer.provide(Plugin.defaultLayer),
213
+ Layer.provide(status),
214
+ Layer.provide(bus),
215
+ Layer.provide(Config.defaultLayer),
216
+ ),
217
+ )
218
+ }
219
+
220
+ function wait(ms = 50) {
221
+ return new Promise((resolve) => setTimeout(resolve, ms))
222
+ }
223
+
224
+ function defer() {
225
+ let resolve!: () => void
226
+ const promise = new Promise<void>((done) => {
227
+ resolve = done
228
+ })
229
+ return { promise, resolve }
230
+ }
231
+
232
+ function plugin(ready: ReturnType<typeof defer>) {
233
+ return Layer.mock(Plugin.Service)({
234
+ trigger: <Name extends string, Input, Output>(name: Name, _input: Input, output: Output) => {
235
+ if (name !== "experimental.session.compacting") return Effect.succeed(output)
236
+ return Effect.sync(() => ready.resolve()).pipe(Effect.andThen(Effect.never), Effect.as(output))
237
+ },
238
+ list: () => Effect.succeed([]),
239
+ init: () => Effect.void,
240
+ })
241
+ }
242
+
243
+ describe("session.compaction.isOverflow", () => {
244
+ test("returns true when token count exceeds usable context", async () => {
245
+ await using tmp = await tmpdir()
246
+ await Instance.provide({
247
+ directory: tmp.path,
248
+ fn: async () => {
249
+ const model = createModel({ context: 100_000, output: 32_000 })
250
+ const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
251
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
252
+ },
253
+ })
254
+ })
255
+
256
+ test("returns false when token count within usable context", async () => {
257
+ await using tmp = await tmpdir()
258
+ await Instance.provide({
259
+ directory: tmp.path,
260
+ fn: async () => {
261
+ const model = createModel({ context: 200_000, output: 32_000 })
262
+ const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
263
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
264
+ },
265
+ })
266
+ })
267
+
268
+ test("includes cache.read in token count", async () => {
269
+ await using tmp = await tmpdir()
270
+ await Instance.provide({
271
+ directory: tmp.path,
272
+ fn: async () => {
273
+ const model = createModel({ context: 100_000, output: 32_000 })
274
+ const tokens = { input: 60_000, output: 10_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
275
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
276
+ },
277
+ })
278
+ })
279
+
280
+ test("respects input limit for input caps", async () => {
281
+ await using tmp = await tmpdir()
282
+ await Instance.provide({
283
+ directory: tmp.path,
284
+ fn: async () => {
285
+ const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
286
+ const tokens = { input: 271_000, output: 1_000, reasoning: 0, cache: { read: 2_000, write: 0 } }
287
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
288
+ },
289
+ })
290
+ })
291
+
292
+ test("returns false when input/output are within input caps", async () => {
293
+ await using tmp = await tmpdir()
294
+ await Instance.provide({
295
+ directory: tmp.path,
296
+ fn: async () => {
297
+ const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
298
+ const tokens = { input: 200_000, output: 20_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
299
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
300
+ },
301
+ })
302
+ })
303
+
304
+ test("returns false when output within limit with input caps", async () => {
305
+ await using tmp = await tmpdir()
306
+ await Instance.provide({
307
+ directory: tmp.path,
308
+ fn: async () => {
309
+ const model = createModel({ context: 200_000, input: 120_000, output: 10_000 })
310
+ const tokens = { input: 50_000, output: 9_999, reasoning: 0, cache: { read: 0, write: 0 } }
311
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
312
+ },
313
+ })
314
+ })
315
+
316
+ // ─── Bug reproduction tests ───────────────────────────────────────────
317
+ // These tests demonstrate that when limit.input is set, isOverflow()
318
+ // does not subtract any headroom for the next model response. This means
319
+ // compaction only triggers AFTER we've already consumed the full input
320
+ // budget, leaving zero room for the next API call's output tokens.
321
+ //
322
+ // Compare: without limit.input, usable = context - output (reserves space).
323
+ // With limit.input, usable = limit.input (reserves nothing).
324
+ //
325
+ // Related issues: #10634, #8089, #11086, #12621
326
+ // Open PRs: #6875, #12924
327
+
328
+ test("BUG: no headroom when limit.input is set — compaction should trigger near boundary but does not", async () => {
329
+ await using tmp = await tmpdir()
330
+ await Instance.provide({
331
+ directory: tmp.path,
332
+ fn: async () => {
333
+ // Simulate Claude with prompt caching: input limit = 200K, output limit = 32K
334
+ const model = createModel({ context: 200_000, input: 200_000, output: 32_000 })
335
+
336
+ // We've used 198K tokens total. Only 2K under the input limit.
337
+ // On the next turn, the full conversation (198K) becomes input,
338
+ // plus the model needs room to generate output — this WILL overflow.
339
+ const tokens = { input: 180_000, output: 15_000, reasoning: 0, cache: { read: 3_000, write: 0 } }
340
+ // count = 180K + 3K + 15K = 198K
341
+ // usable = limit.input = 200K (no output subtracted!)
342
+ // 198K > 200K = false → no compaction triggered
343
+
344
+ // WITHOUT limit.input: usable = 200K - 32K = 168K, and 198K > 168K = true ✓
345
+ // WITH limit.input: usable = 200K, and 198K > 200K = false ✗
346
+
347
+ // With 198K used and only 2K headroom, the next turn will overflow.
348
+ // Compaction MUST trigger here.
349
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
350
+ },
351
+ })
352
+ })
353
+
354
+ test("BUG: without limit.input, same token count correctly triggers compaction", async () => {
355
+ await using tmp = await tmpdir()
356
+ await Instance.provide({
357
+ directory: tmp.path,
358
+ fn: async () => {
359
+ // Same model but without limit.input — uses context - output instead
360
+ const model = createModel({ context: 200_000, output: 32_000 })
361
+
362
+ // Same token usage as above
363
+ const tokens = { input: 180_000, output: 15_000, reasoning: 0, cache: { read: 3_000, write: 0 } }
364
+ // count = 198K
365
+ // usable = context - output = 200K - 32K = 168K
366
+ // 198K > 168K = true → compaction correctly triggered
367
+
368
+ const result = await SessionCompaction.isOverflow({ tokens, model })
369
+ expect(result).toBe(true) // ← Correct: headroom is reserved
370
+ },
371
+ })
372
+ })
373
+
374
+ test("BUG: asymmetry — limit.input model allows 30K more usage before compaction than equivalent model without it", async () => {
375
+ await using tmp = await tmpdir()
376
+ await Instance.provide({
377
+ directory: tmp.path,
378
+ fn: async () => {
379
+ // Two models with identical context/output limits, differing only in limit.input
380
+ const withInputLimit = createModel({ context: 200_000, input: 200_000, output: 32_000 })
381
+ const withoutInputLimit = createModel({ context: 200_000, output: 32_000 })
382
+
383
+ // 170K total tokens — well above context-output (168K) but below input limit (200K)
384
+ const tokens = { input: 166_000, output: 10_000, reasoning: 0, cache: { read: 5_000, write: 0 } }
385
+
386
+ const withLimit = await SessionCompaction.isOverflow({ tokens, model: withInputLimit })
387
+ const withoutLimit = await SessionCompaction.isOverflow({ tokens, model: withoutInputLimit })
388
+
389
+ // Both models have identical real capacity — they should agree:
390
+ expect(withLimit).toBe(true) // should compact (170K leaves no room for 32K output)
391
+ expect(withoutLimit).toBe(true) // correctly compacts (170K > 168K)
392
+ },
393
+ })
394
+ })
395
+
396
+ test("returns false when model context limit is 0", async () => {
397
+ await using tmp = await tmpdir()
398
+ await Instance.provide({
399
+ directory: tmp.path,
400
+ fn: async () => {
401
+ const model = createModel({ context: 0, output: 32_000 })
402
+ const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
403
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
404
+ },
405
+ })
406
+ })
407
+
408
+ test("returns false when compaction.auto is disabled", async () => {
409
+ await using tmp = await tmpdir({
410
+ init: async (dir) => {
411
+ await Bun.write(
412
+ path.join(dir, "opencode.json"),
413
+ JSON.stringify({
414
+ compaction: { auto: false },
415
+ }),
416
+ )
417
+ },
418
+ })
419
+ await Instance.provide({
420
+ directory: tmp.path,
421
+ fn: async () => {
422
+ const model = createModel({ context: 100_000, output: 32_000 })
423
+ const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
424
+ expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
425
+ },
426
+ })
427
+ })
428
+ })
429
+
430
+ describe("session.compaction.create", () => {
431
+ test("creates a compaction user message and part", async () => {
432
+ await using tmp = await tmpdir()
433
+ await Instance.provide({
434
+ directory: tmp.path,
435
+ fn: async () => {
436
+ const session = await Session.create({})
437
+
438
+ await SessionCompaction.create({
439
+ sessionID: session.id,
440
+ agent: "build",
441
+ model: ref,
442
+ auto: true,
443
+ overflow: true,
444
+ })
445
+
446
+ const msgs = await Session.messages({ sessionID: session.id })
447
+ expect(msgs).toHaveLength(1)
448
+ expect(msgs[0].info.role).toBe("user")
449
+ expect(msgs[0].parts).toHaveLength(1)
450
+ expect(msgs[0].parts[0]).toMatchObject({
451
+ type: "compaction",
452
+ auto: true,
453
+ overflow: true,
454
+ })
455
+ },
456
+ })
457
+ })
458
+ })
459
+
460
+ describe("session.compaction.prune", () => {
461
+ test("compacts old completed tool output", async () => {
462
+ await using tmp = await tmpdir()
463
+ await Instance.provide({
464
+ directory: tmp.path,
465
+ fn: async () => {
466
+ const session = await Session.create({})
467
+ const a = await user(session.id, "first")
468
+ const b = await assistant(session.id, a.id, tmp.path)
469
+ await tool(session.id, b.id, "bash", "x".repeat(200_000))
470
+ await user(session.id, "second")
471
+ await user(session.id, "third")
472
+
473
+ await SessionCompaction.prune({ sessionID: session.id })
474
+
475
+ const msgs = await Session.messages({ sessionID: session.id })
476
+ const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
477
+ expect(part?.type).toBe("tool")
478
+ expect(part?.state.status).toBe("completed")
479
+ if (part?.type === "tool" && part.state.status === "completed") {
480
+ expect(part.state.time.compacted).toBeNumber()
481
+ }
482
+ },
483
+ })
484
+ })
485
+
486
+ test("skips protected skill tool output", async () => {
487
+ await using tmp = await tmpdir()
488
+ await Instance.provide({
489
+ directory: tmp.path,
490
+ fn: async () => {
491
+ const session = await Session.create({})
492
+ const a = await user(session.id, "first")
493
+ const b = await assistant(session.id, a.id, tmp.path)
494
+ await tool(session.id, b.id, "skill", "x".repeat(200_000))
495
+ await user(session.id, "second")
496
+ await user(session.id, "third")
497
+
498
+ await SessionCompaction.prune({ sessionID: session.id })
499
+
500
+ const msgs = await Session.messages({ sessionID: session.id })
501
+ const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
502
+ expect(part?.type).toBe("tool")
503
+ if (part?.type === "tool" && part.state.status === "completed") {
504
+ expect(part.state.time.compacted).toBeUndefined()
505
+ }
506
+ },
507
+ })
508
+ })
509
+ })
510
+
511
+ describe("session.compaction.process", () => {
512
+ test("publishes compacted event on continue", async () => {
513
+ await using tmp = await tmpdir()
514
+ await Instance.provide({
515
+ directory: tmp.path,
516
+ fn: async () => {
517
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
518
+
519
+ const session = await Session.create({})
520
+ const msg = await user(session.id, "hello")
521
+ const msgs = await Session.messages({ sessionID: session.id })
522
+ const done = defer()
523
+ let seen = false
524
+ const rt = runtime("continue")
525
+ let unsub: (() => void) | undefined
526
+ try {
527
+ unsub = await rt.runPromise(
528
+ Bus.Service.use((svc) =>
529
+ svc.subscribeCallback(SessionCompaction.Event.Compacted, (evt) => {
530
+ if (evt.properties.sessionID !== session.id) return
531
+ seen = true
532
+ done.resolve()
533
+ }),
534
+ ),
535
+ )
536
+
537
+ const result = await rt.runPromise(
538
+ SessionCompaction.Service.use((svc) =>
539
+ svc.process({
540
+ parentID: msg.id,
541
+ messages: msgs,
542
+ sessionID: session.id,
543
+ abort: new AbortController().signal,
544
+ auto: false,
545
+ }),
546
+ ),
547
+ )
548
+
549
+ await Promise.race([
550
+ done.promise,
551
+ wait(500).then(() => {
552
+ throw new Error("timed out waiting for compacted event")
553
+ }),
554
+ ])
555
+ expect(result).toBe("continue")
556
+ expect(seen).toBe(true)
557
+ } finally {
558
+ unsub?.()
559
+ await rt.dispose()
560
+ }
561
+ },
562
+ })
563
+ })
564
+
565
+ test("marks summary message as errored on compact result", async () => {
566
+ await using tmp = await tmpdir()
567
+ await Instance.provide({
568
+ directory: tmp.path,
569
+ fn: async () => {
570
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
571
+
572
+ const session = await Session.create({})
573
+ const msg = await user(session.id, "hello")
574
+ const rt = runtime("compact")
575
+ try {
576
+ const msgs = await Session.messages({ sessionID: session.id })
577
+ const result = await rt.runPromise(
578
+ SessionCompaction.Service.use((svc) =>
579
+ svc.process({
580
+ parentID: msg.id,
581
+ messages: msgs,
582
+ sessionID: session.id,
583
+ abort: new AbortController().signal,
584
+ auto: false,
585
+ }),
586
+ ),
587
+ )
588
+
589
+ const summary = (await Session.messages({ sessionID: session.id })).find(
590
+ (msg) => msg.info.role === "assistant" && msg.info.summary,
591
+ )
592
+
593
+ expect(result).toBe("stop")
594
+ expect(summary?.info.role).toBe("assistant")
595
+ if (summary?.info.role === "assistant") {
596
+ expect(summary.info.finish).toBe("error")
597
+ expect(JSON.stringify(summary.info.error)).toContain("Session too large to compact")
598
+ }
599
+ } finally {
600
+ await rt.dispose()
601
+ }
602
+ },
603
+ })
604
+ })
605
+
606
+ test("adds synthetic continue prompt when auto is enabled", async () => {
607
+ await using tmp = await tmpdir()
608
+ await Instance.provide({
609
+ directory: tmp.path,
610
+ fn: async () => {
611
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
612
+
613
+ const session = await Session.create({})
614
+ const msg = await user(session.id, "hello")
615
+ const rt = runtime("continue")
616
+ try {
617
+ const msgs = await Session.messages({ sessionID: session.id })
618
+ const result = await rt.runPromise(
619
+ SessionCompaction.Service.use((svc) =>
620
+ svc.process({
621
+ parentID: msg.id,
622
+ messages: msgs,
623
+ sessionID: session.id,
624
+ abort: new AbortController().signal,
625
+ auto: true,
626
+ }),
627
+ ),
628
+ )
629
+
630
+ const all = await Session.messages({ sessionID: session.id })
631
+ const last = all.at(-1)
632
+
633
+ expect(result).toBe("continue")
634
+ expect(last?.info.role).toBe("user")
635
+ expect(last?.parts[0]).toMatchObject({
636
+ type: "text",
637
+ synthetic: true,
638
+ })
639
+ if (last?.parts[0]?.type === "text") {
640
+ expect(last.parts[0].text).toContain("Continue if you have next steps")
641
+ }
642
+ } finally {
643
+ await rt.dispose()
644
+ }
645
+ },
646
+ })
647
+ })
648
+
649
+ test("replays the prior user turn on overflow when earlier context exists", async () => {
650
+ await using tmp = await tmpdir()
651
+ await Instance.provide({
652
+ directory: tmp.path,
653
+ fn: async () => {
654
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
655
+
656
+ const session = await Session.create({})
657
+ await user(session.id, "root")
658
+ const replay = await user(session.id, "image")
659
+ await Session.updatePart({
660
+ id: PartID.ascending(),
661
+ messageID: replay.id,
662
+ sessionID: session.id,
663
+ type: "file",
664
+ mime: "image/png",
665
+ filename: "cat.png",
666
+ url: "https://example.com/cat.png",
667
+ })
668
+ const msg = await user(session.id, "current")
669
+ const rt = runtime("continue")
670
+ try {
671
+ const msgs = await Session.messages({ sessionID: session.id })
672
+ const result = await rt.runPromise(
673
+ SessionCompaction.Service.use((svc) =>
674
+ svc.process({
675
+ parentID: msg.id,
676
+ messages: msgs,
677
+ sessionID: session.id,
678
+ abort: new AbortController().signal,
679
+ auto: true,
680
+ overflow: true,
681
+ }),
682
+ ),
683
+ )
684
+
685
+ const last = (await Session.messages({ sessionID: session.id })).at(-1)
686
+
687
+ expect(result).toBe("continue")
688
+ expect(last?.info.role).toBe("user")
689
+ expect(last?.parts.some((part) => part.type === "file")).toBe(false)
690
+ expect(
691
+ last?.parts.some((part) => part.type === "text" && part.text.includes("Attached image/png: cat.png")),
692
+ ).toBe(true)
693
+ } finally {
694
+ await rt.dispose()
695
+ }
696
+ },
697
+ })
698
+ })
699
+
700
+ test("falls back to overflow guidance when no replayable turn exists", async () => {
701
+ await using tmp = await tmpdir()
702
+ await Instance.provide({
703
+ directory: tmp.path,
704
+ fn: async () => {
705
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
706
+
707
+ const session = await Session.create({})
708
+ await user(session.id, "earlier")
709
+ const msg = await user(session.id, "current")
710
+
711
+ const rt = runtime("continue")
712
+ try {
713
+ const msgs = await Session.messages({ sessionID: session.id })
714
+ const result = await rt.runPromise(
715
+ SessionCompaction.Service.use((svc) =>
716
+ svc.process({
717
+ parentID: msg.id,
718
+ messages: msgs,
719
+ sessionID: session.id,
720
+ abort: new AbortController().signal,
721
+ auto: true,
722
+ overflow: true,
723
+ }),
724
+ ),
725
+ )
726
+
727
+ const last = (await Session.messages({ sessionID: session.id })).at(-1)
728
+
729
+ expect(result).toBe("continue")
730
+ expect(last?.info.role).toBe("user")
731
+ if (last?.parts[0]?.type === "text") {
732
+ expect(last.parts[0].text).toContain("previous request exceeded the provider's size limit")
733
+ }
734
+ } finally {
735
+ await rt.dispose()
736
+ }
737
+ },
738
+ })
739
+ })
740
+
741
+ test("stops quickly when aborted during retry backoff", async () => {
742
+ const stub = llm()
743
+ const ready = defer()
744
+ stub.push(
745
+ Stream.fromAsyncIterable(
746
+ {
747
+ async *[Symbol.asyncIterator]() {
748
+ yield { type: "start" } as LLM.Event
749
+ throw new APICallError({
750
+ message: "boom",
751
+ url: "https://example.com/v1/chat/completions",
752
+ requestBodyValues: {},
753
+ statusCode: 503,
754
+ responseHeaders: { "retry-after-ms": "10000" },
755
+ responseBody: '{"error":"boom"}',
756
+ isRetryable: true,
757
+ })
758
+ },
759
+ },
760
+ (err) => err,
761
+ ),
762
+ )
763
+
764
+ await using tmp = await tmpdir({ git: true })
765
+ await Instance.provide({
766
+ directory: tmp.path,
767
+ fn: async () => {
768
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
769
+
770
+ const session = await Session.create({})
771
+ const msg = await user(session.id, "hello")
772
+ const msgs = await Session.messages({ sessionID: session.id })
773
+ const abort = new AbortController()
774
+ const rt = liveRuntime(stub.layer)
775
+ let off: (() => void) | undefined
776
+ let run: Promise<"continue" | "stop"> | undefined
777
+ try {
778
+ off = await rt.runPromise(
779
+ Bus.Service.use((svc) =>
780
+ svc.subscribeCallback(SessionStatus.Event.Status, (evt) => {
781
+ if (evt.properties.sessionID !== session.id) return
782
+ if (evt.properties.status.type !== "retry") return
783
+ ready.resolve()
784
+ }),
785
+ ),
786
+ )
787
+
788
+ run = rt
789
+ .runPromiseExit(
790
+ SessionCompaction.Service.use((svc) =>
791
+ svc.process({
792
+ parentID: msg.id,
793
+ messages: msgs,
794
+ sessionID: session.id,
795
+ abort: abort.signal,
796
+ auto: false,
797
+ }),
798
+ ),
799
+ { signal: abort.signal },
800
+ )
801
+ .then((exit) => {
802
+ if (Exit.isFailure(exit)) {
803
+ if (Cause.hasInterrupts(exit.cause) && abort.signal.aborted) return "stop"
804
+ throw Cause.squash(exit.cause)
805
+ }
806
+ return exit.value
807
+ })
808
+
809
+ await Promise.race([
810
+ ready.promise,
811
+ wait(1000).then(() => {
812
+ throw new Error("timed out waiting for retry status")
813
+ }),
814
+ ])
815
+
816
+ const start = Date.now()
817
+ abort.abort()
818
+ const result = await Promise.race([
819
+ run.then((value) => ({ kind: "done" as const, value, ms: Date.now() - start })),
820
+ wait(250).then(() => ({ kind: "timeout" as const })),
821
+ ])
822
+
823
+ expect(result.kind).toBe("done")
824
+ if (result.kind === "done") {
825
+ expect(result.value).toBe("stop")
826
+ expect(result.ms).toBeLessThan(250)
827
+ }
828
+ } finally {
829
+ off?.()
830
+ abort.abort()
831
+ await rt.dispose()
832
+ await run?.catch(() => undefined)
833
+ }
834
+ },
835
+ })
836
+ })
837
+
838
+ test("does not leave a summary assistant when aborted before processor setup", async () => {
839
+ const ready = defer()
840
+
841
+ await using tmp = await tmpdir({ git: true })
842
+ await Instance.provide({
843
+ directory: tmp.path,
844
+ fn: async () => {
845
+ spyOn(ProviderModule.Provider, "getModel").mockResolvedValue(createModel({ context: 100_000, output: 32_000 }))
846
+
847
+ const session = await Session.create({})
848
+ const msg = await user(session.id, "hello")
849
+ const msgs = await Session.messages({ sessionID: session.id })
850
+ const abort = new AbortController()
851
+ const rt = runtime("continue", plugin(ready))
852
+ let run: Promise<"continue" | "stop"> | undefined
853
+ try {
854
+ run = rt
855
+ .runPromiseExit(
856
+ SessionCompaction.Service.use((svc) =>
857
+ svc.process({
858
+ parentID: msg.id,
859
+ messages: msgs,
860
+ sessionID: session.id,
861
+ abort: abort.signal,
862
+ auto: false,
863
+ }),
864
+ ),
865
+ { signal: abort.signal },
866
+ )
867
+ .then((exit) => {
868
+ if (Exit.isFailure(exit)) {
869
+ if (Cause.hasInterrupts(exit.cause) && abort.signal.aborted) return "stop"
870
+ throw Cause.squash(exit.cause)
871
+ }
872
+ return exit.value
873
+ })
874
+
875
+ await Promise.race([
876
+ ready.promise,
877
+ wait(1000).then(() => {
878
+ throw new Error("timed out waiting for compaction hook")
879
+ }),
880
+ ])
881
+
882
+ abort.abort()
883
+ expect(await run).toBe("stop")
884
+
885
+ const all = await Session.messages({ sessionID: session.id })
886
+ expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false)
887
+ } finally {
888
+ abort.abort()
889
+ await rt.dispose()
890
+ await run?.catch(() => undefined)
891
+ }
892
+ },
893
+ })
894
+ })
895
+ })
896
+
897
+ describe("util.token.estimate", () => {
898
+ test("estimates tokens from text (4 chars per token)", () => {
899
+ const text = "x".repeat(4000)
900
+ expect(Token.estimate(text)).toBe(1000)
901
+ })
902
+
903
+ test("estimates tokens from larger text", () => {
904
+ const text = "y".repeat(20_000)
905
+ expect(Token.estimate(text)).toBe(5000)
906
+ })
907
+
908
+ test("returns 0 for empty string", () => {
909
+ expect(Token.estimate("")).toBe(0)
910
+ })
911
+ })
912
+
913
+ describe("session.getUsage", () => {
914
+ test("normalizes standard usage to token format", () => {
915
+ const model = createModel({ context: 100_000, output: 32_000 })
916
+ const result = Session.getUsage({
917
+ model,
918
+ usage: {
919
+ inputTokens: 1000,
920
+ outputTokens: 500,
921
+ totalTokens: 1500,
922
+ },
923
+ })
924
+
925
+ expect(result.tokens.input).toBe(1000)
926
+ expect(result.tokens.output).toBe(500)
927
+ expect(result.tokens.reasoning).toBe(0)
928
+ expect(result.tokens.cache.read).toBe(0)
929
+ expect(result.tokens.cache.write).toBe(0)
930
+ })
931
+
932
+ test("extracts cached tokens to cache.read", () => {
933
+ const model = createModel({ context: 100_000, output: 32_000 })
934
+ const result = Session.getUsage({
935
+ model,
936
+ usage: {
937
+ inputTokens: 1000,
938
+ outputTokens: 500,
939
+ totalTokens: 1500,
940
+ cachedInputTokens: 200,
941
+ },
942
+ })
943
+
944
+ expect(result.tokens.input).toBe(800)
945
+ expect(result.tokens.cache.read).toBe(200)
946
+ })
947
+
948
+ test("handles anthropic cache write metadata", () => {
949
+ const model = createModel({ context: 100_000, output: 32_000 })
950
+ const result = Session.getUsage({
951
+ model,
952
+ usage: {
953
+ inputTokens: 1000,
954
+ outputTokens: 500,
955
+ totalTokens: 1500,
956
+ },
957
+ metadata: {
958
+ anthropic: {
959
+ cacheCreationInputTokens: 300,
960
+ },
961
+ },
962
+ })
963
+
964
+ expect(result.tokens.cache.write).toBe(300)
965
+ })
966
+
967
+ test("subtracts cached tokens for anthropic provider", () => {
968
+ const model = createModel({ context: 100_000, output: 32_000 })
969
+ // AI SDK v6 normalizes inputTokens to include cached tokens for all providers
970
+ const result = Session.getUsage({
971
+ model,
972
+ usage: {
973
+ inputTokens: 1000,
974
+ outputTokens: 500,
975
+ totalTokens: 1500,
976
+ cachedInputTokens: 200,
977
+ },
978
+ metadata: {
979
+ anthropic: {},
980
+ },
981
+ })
982
+
983
+ expect(result.tokens.input).toBe(800)
984
+ expect(result.tokens.cache.read).toBe(200)
985
+ })
986
+
987
+ test("handles reasoning tokens", () => {
988
+ const model = createModel({ context: 100_000, output: 32_000 })
989
+ const result = Session.getUsage({
990
+ model,
991
+ usage: {
992
+ inputTokens: 1000,
993
+ outputTokens: 500,
994
+ totalTokens: 1500,
995
+ reasoningTokens: 100,
996
+ },
997
+ })
998
+
999
+ expect(result.tokens.reasoning).toBe(100)
1000
+ })
1001
+
1002
+ test("handles undefined optional values gracefully", () => {
1003
+ const model = createModel({ context: 100_000, output: 32_000 })
1004
+ const result = Session.getUsage({
1005
+ model,
1006
+ usage: {
1007
+ inputTokens: 0,
1008
+ outputTokens: 0,
1009
+ totalTokens: 0,
1010
+ },
1011
+ })
1012
+
1013
+ expect(result.tokens.input).toBe(0)
1014
+ expect(result.tokens.output).toBe(0)
1015
+ expect(result.tokens.reasoning).toBe(0)
1016
+ expect(result.tokens.cache.read).toBe(0)
1017
+ expect(result.tokens.cache.write).toBe(0)
1018
+ expect(Number.isNaN(result.cost)).toBe(false)
1019
+ })
1020
+
1021
+ test("calculates cost correctly", () => {
1022
+ const model = createModel({
1023
+ context: 100_000,
1024
+ output: 32_000,
1025
+ cost: {
1026
+ input: 3,
1027
+ output: 15,
1028
+ cache: { read: 0.3, write: 3.75 },
1029
+ },
1030
+ })
1031
+ const result = Session.getUsage({
1032
+ model,
1033
+ usage: {
1034
+ inputTokens: 1_000_000,
1035
+ outputTokens: 100_000,
1036
+ totalTokens: 1_100_000,
1037
+ },
1038
+ })
1039
+
1040
+ expect(result.cost).toBe(3 + 1.5)
1041
+ })
1042
+
1043
+ test.each(["@ai-sdk/anthropic", "@ai-sdk/amazon-bedrock", "@ai-sdk/google-vertex/anthropic"])(
1044
+ "computes total from components for %s models",
1045
+ (npm) => {
1046
+ const model = createModel({ context: 100_000, output: 32_000, npm })
1047
+ // AI SDK v6: inputTokens includes cached tokens for all providers
1048
+ const usage = {
1049
+ inputTokens: 1000,
1050
+ outputTokens: 500,
1051
+ totalTokens: 1500,
1052
+ cachedInputTokens: 200,
1053
+ }
1054
+ if (npm === "@ai-sdk/amazon-bedrock") {
1055
+ const result = Session.getUsage({
1056
+ model,
1057
+ usage,
1058
+ metadata: {
1059
+ bedrock: {
1060
+ usage: {
1061
+ cacheWriteInputTokens: 300,
1062
+ },
1063
+ },
1064
+ },
1065
+ })
1066
+
1067
+ // inputTokens (1000) includes cache, so adjusted = 1000 - 200 - 300 = 500
1068
+ expect(result.tokens.input).toBe(500)
1069
+ expect(result.tokens.cache.read).toBe(200)
1070
+ expect(result.tokens.cache.write).toBe(300)
1071
+ // total = adjusted (500) + output (500) + cacheRead (200) + cacheWrite (300)
1072
+ expect(result.tokens.total).toBe(1500)
1073
+ return
1074
+ }
1075
+
1076
+ const result = Session.getUsage({
1077
+ model,
1078
+ usage,
1079
+ metadata: {
1080
+ anthropic: {
1081
+ cacheCreationInputTokens: 300,
1082
+ },
1083
+ },
1084
+ })
1085
+
1086
+ // inputTokens (1000) includes cache, so adjusted = 1000 - 200 - 300 = 500
1087
+ expect(result.tokens.input).toBe(500)
1088
+ expect(result.tokens.cache.read).toBe(200)
1089
+ expect(result.tokens.cache.write).toBe(300)
1090
+ // total = adjusted (500) + output (500) + cacheRead (200) + cacheWrite (300)
1091
+ expect(result.tokens.total).toBe(1500)
1092
+ },
1093
+ )
1094
+ })