@stonerzju/opencode 1.2.17 → 1.2.18

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 (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,78 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import path from "path"
3
- import { Session } from "../../src/session"
4
- import { Log } from "../../src/util/log"
5
- import { Instance } from "../../src/project/instance"
6
- import { Server } from "../../src/server/server"
7
-
8
- const projectRoot = path.join(__dirname, "../..")
9
- Log.init({ print: false })
10
-
11
- describe("tui.selectSession endpoint", () => {
12
- test("should return 200 when called with valid session", async () => {
13
- await Instance.provide({
14
- directory: projectRoot,
15
- fn: async () => {
16
- // #given
17
- const session = await Session.create({})
18
-
19
- // #when
20
- const app = Server.App()
21
- const response = await app.request("/tui/select-session", {
22
- method: "POST",
23
- headers: { "Content-Type": "application/json" },
24
- body: JSON.stringify({ sessionID: session.id }),
25
- })
26
-
27
- // #then
28
- expect(response.status).toBe(200)
29
- const body = await response.json()
30
- expect(body).toBe(true)
31
-
32
- await Session.remove(session.id)
33
- },
34
- })
35
- })
36
-
37
- test("should return 404 when session does not exist", async () => {
38
- await Instance.provide({
39
- directory: projectRoot,
40
- fn: async () => {
41
- // #given
42
- const nonExistentSessionID = "ses_nonexistent123"
43
-
44
- // #when
45
- const app = Server.App()
46
- const response = await app.request("/tui/select-session", {
47
- method: "POST",
48
- headers: { "Content-Type": "application/json" },
49
- body: JSON.stringify({ sessionID: nonExistentSessionID }),
50
- })
51
-
52
- // #then
53
- expect(response.status).toBe(404)
54
- },
55
- })
56
- })
57
-
58
- test("should return 400 when session ID format is invalid", async () => {
59
- await Instance.provide({
60
- directory: projectRoot,
61
- fn: async () => {
62
- // #given
63
- const invalidSessionID = "invalid_session_id"
64
-
65
- // #when
66
- const app = Server.App()
67
- const response = await app.request("/tui/select-session", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({ sessionID: invalidSessionID }),
71
- })
72
-
73
- // #then
74
- expect(response.status).toBe(400)
75
- },
76
- })
77
- })
78
- })
@@ -1,423 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import path from "path"
3
- import { SessionCompaction } from "../../src/session/compaction"
4
- import { Token } from "../../src/util/token"
5
- import { Instance } from "../../src/project/instance"
6
- import { Log } from "../../src/util/log"
7
- import { tmpdir } from "../fixture/fixture"
8
- import { Session } from "../../src/session"
9
- import type { Provider } from "../../src/provider/provider"
10
-
11
- Log.init({ print: false })
12
-
13
- function createModel(opts: {
14
- context: number
15
- output: number
16
- input?: number
17
- cost?: Provider.Model["cost"]
18
- npm?: string
19
- }): Provider.Model {
20
- return {
21
- id: "test-model",
22
- providerID: "test",
23
- name: "Test",
24
- limit: {
25
- context: opts.context,
26
- input: opts.input,
27
- output: opts.output,
28
- },
29
- cost: opts.cost ?? { input: 0, output: 0, cache: { read: 0, write: 0 } },
30
- capabilities: {
31
- toolcall: true,
32
- attachment: false,
33
- reasoning: false,
34
- temperature: true,
35
- input: { text: true, image: false, audio: false, video: false },
36
- output: { text: true, image: false, audio: false, video: false },
37
- },
38
- api: { npm: opts.npm ?? "@ai-sdk/anthropic" },
39
- options: {},
40
- } as Provider.Model
41
- }
42
-
43
- describe("session.compaction.isOverflow", () => {
44
- test("returns true when token count exceeds usable context", async () => {
45
- await using tmp = await tmpdir()
46
- await Instance.provide({
47
- directory: tmp.path,
48
- fn: async () => {
49
- const model = createModel({ context: 100_000, output: 32_000 })
50
- const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
51
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
52
- },
53
- })
54
- })
55
-
56
- test("returns false when token count within usable context", async () => {
57
- await using tmp = await tmpdir()
58
- await Instance.provide({
59
- directory: tmp.path,
60
- fn: async () => {
61
- const model = createModel({ context: 200_000, output: 32_000 })
62
- const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
63
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
64
- },
65
- })
66
- })
67
-
68
- test("includes cache.read in token count", async () => {
69
- await using tmp = await tmpdir()
70
- await Instance.provide({
71
- directory: tmp.path,
72
- fn: async () => {
73
- const model = createModel({ context: 100_000, output: 32_000 })
74
- const tokens = { input: 60_000, output: 10_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
75
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
76
- },
77
- })
78
- })
79
-
80
- test("respects input limit for input caps", async () => {
81
- await using tmp = await tmpdir()
82
- await Instance.provide({
83
- directory: tmp.path,
84
- fn: async () => {
85
- const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
86
- const tokens = { input: 271_000, output: 1_000, reasoning: 0, cache: { read: 2_000, write: 0 } }
87
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
88
- },
89
- })
90
- })
91
-
92
- test("returns false when input/output are within input caps", async () => {
93
- await using tmp = await tmpdir()
94
- await Instance.provide({
95
- directory: tmp.path,
96
- fn: async () => {
97
- const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
98
- const tokens = { input: 200_000, output: 20_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
99
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
100
- },
101
- })
102
- })
103
-
104
- test("returns false when output within limit with input caps", async () => {
105
- await using tmp = await tmpdir()
106
- await Instance.provide({
107
- directory: tmp.path,
108
- fn: async () => {
109
- const model = createModel({ context: 200_000, input: 120_000, output: 10_000 })
110
- const tokens = { input: 50_000, output: 9_999, reasoning: 0, cache: { read: 0, write: 0 } }
111
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
112
- },
113
- })
114
- })
115
-
116
- // ─── Bug reproduction tests ───────────────────────────────────────────
117
- // These tests demonstrate that when limit.input is set, isOverflow()
118
- // does not subtract any headroom for the next model response. This means
119
- // compaction only triggers AFTER we've already consumed the full input
120
- // budget, leaving zero room for the next API call's output tokens.
121
- //
122
- // Compare: without limit.input, usable = context - output (reserves space).
123
- // With limit.input, usable = limit.input (reserves nothing).
124
- //
125
- // Related issues: #10634, #8089, #11086, #12621
126
- // Open PRs: #6875, #12924
127
-
128
- test("BUG: no headroom when limit.input is set — compaction should trigger near boundary but does not", async () => {
129
- await using tmp = await tmpdir()
130
- await Instance.provide({
131
- directory: tmp.path,
132
- fn: async () => {
133
- // Simulate Claude with prompt caching: input limit = 200K, output limit = 32K
134
- const model = createModel({ context: 200_000, input: 200_000, output: 32_000 })
135
-
136
- // We've used 198K tokens total. Only 2K under the input limit.
137
- // On the next turn, the full conversation (198K) becomes input,
138
- // plus the model needs room to generate output — this WILL overflow.
139
- const tokens = { input: 180_000, output: 15_000, reasoning: 0, cache: { read: 3_000, write: 0 } }
140
- // count = 180K + 3K + 15K = 198K
141
- // usable = limit.input = 200K (no output subtracted!)
142
- // 198K > 200K = false → no compaction triggered
143
-
144
- // WITHOUT limit.input: usable = 200K - 32K = 168K, and 198K > 168K = true ✓
145
- // WITH limit.input: usable = 200K, and 198K > 200K = false ✗
146
-
147
- // With 198K used and only 2K headroom, the next turn will overflow.
148
- // Compaction MUST trigger here.
149
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
150
- },
151
- })
152
- })
153
-
154
- test("BUG: without limit.input, same token count correctly triggers compaction", async () => {
155
- await using tmp = await tmpdir()
156
- await Instance.provide({
157
- directory: tmp.path,
158
- fn: async () => {
159
- // Same model but without limit.input — uses context - output instead
160
- const model = createModel({ context: 200_000, output: 32_000 })
161
-
162
- // Same token usage as above
163
- const tokens = { input: 180_000, output: 15_000, reasoning: 0, cache: { read: 3_000, write: 0 } }
164
- // count = 198K
165
- // usable = context - output = 200K - 32K = 168K
166
- // 198K > 168K = true → compaction correctly triggered
167
-
168
- const result = await SessionCompaction.isOverflow({ tokens, model })
169
- expect(result).toBe(true) // ← Correct: headroom is reserved
170
- },
171
- })
172
- })
173
-
174
- test("BUG: asymmetry — limit.input model allows 30K more usage before compaction than equivalent model without it", async () => {
175
- await using tmp = await tmpdir()
176
- await Instance.provide({
177
- directory: tmp.path,
178
- fn: async () => {
179
- // Two models with identical context/output limits, differing only in limit.input
180
- const withInputLimit = createModel({ context: 200_000, input: 200_000, output: 32_000 })
181
- const withoutInputLimit = createModel({ context: 200_000, output: 32_000 })
182
-
183
- // 170K total tokens — well above context-output (168K) but below input limit (200K)
184
- const tokens = { input: 166_000, output: 10_000, reasoning: 0, cache: { read: 5_000, write: 0 } }
185
-
186
- const withLimit = await SessionCompaction.isOverflow({ tokens, model: withInputLimit })
187
- const withoutLimit = await SessionCompaction.isOverflow({ tokens, model: withoutInputLimit })
188
-
189
- // Both models have identical real capacity — they should agree:
190
- expect(withLimit).toBe(true) // should compact (170K leaves no room for 32K output)
191
- expect(withoutLimit).toBe(true) // correctly compacts (170K > 168K)
192
- },
193
- })
194
- })
195
-
196
- test("returns false when model context limit is 0", async () => {
197
- await using tmp = await tmpdir()
198
- await Instance.provide({
199
- directory: tmp.path,
200
- fn: async () => {
201
- const model = createModel({ context: 0, output: 32_000 })
202
- const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
203
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
204
- },
205
- })
206
- })
207
-
208
- test("returns false when compaction.auto is disabled", async () => {
209
- await using tmp = await tmpdir({
210
- init: async (dir) => {
211
- await Bun.write(
212
- path.join(dir, "opencode.json"),
213
- JSON.stringify({
214
- compaction: { auto: false },
215
- }),
216
- )
217
- },
218
- })
219
- await Instance.provide({
220
- directory: tmp.path,
221
- fn: async () => {
222
- const model = createModel({ context: 100_000, output: 32_000 })
223
- const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
224
- expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
225
- },
226
- })
227
- })
228
- })
229
-
230
- describe("util.token.estimate", () => {
231
- test("estimates tokens from text (4 chars per token)", () => {
232
- const text = "x".repeat(4000)
233
- expect(Token.estimate(text)).toBe(1000)
234
- })
235
-
236
- test("estimates tokens from larger text", () => {
237
- const text = "y".repeat(20_000)
238
- expect(Token.estimate(text)).toBe(5000)
239
- })
240
-
241
- test("returns 0 for empty string", () => {
242
- expect(Token.estimate("")).toBe(0)
243
- })
244
- })
245
-
246
- describe("session.getUsage", () => {
247
- test("normalizes standard usage to token format", () => {
248
- const model = createModel({ context: 100_000, output: 32_000 })
249
- const result = Session.getUsage({
250
- model,
251
- usage: {
252
- inputTokens: 1000,
253
- outputTokens: 500,
254
- totalTokens: 1500,
255
- },
256
- })
257
-
258
- expect(result.tokens.input).toBe(1000)
259
- expect(result.tokens.output).toBe(500)
260
- expect(result.tokens.reasoning).toBe(0)
261
- expect(result.tokens.cache.read).toBe(0)
262
- expect(result.tokens.cache.write).toBe(0)
263
- })
264
-
265
- test("extracts cached tokens to cache.read", () => {
266
- const model = createModel({ context: 100_000, output: 32_000 })
267
- const result = Session.getUsage({
268
- model,
269
- usage: {
270
- inputTokens: 1000,
271
- outputTokens: 500,
272
- totalTokens: 1500,
273
- cachedInputTokens: 200,
274
- },
275
- })
276
-
277
- expect(result.tokens.input).toBe(800)
278
- expect(result.tokens.cache.read).toBe(200)
279
- })
280
-
281
- test("handles anthropic cache write metadata", () => {
282
- const model = createModel({ context: 100_000, output: 32_000 })
283
- const result = Session.getUsage({
284
- model,
285
- usage: {
286
- inputTokens: 1000,
287
- outputTokens: 500,
288
- totalTokens: 1500,
289
- },
290
- metadata: {
291
- anthropic: {
292
- cacheCreationInputTokens: 300,
293
- },
294
- },
295
- })
296
-
297
- expect(result.tokens.cache.write).toBe(300)
298
- })
299
-
300
- test("does not subtract cached tokens for anthropic provider", () => {
301
- const model = createModel({ context: 100_000, output: 32_000 })
302
- const result = Session.getUsage({
303
- model,
304
- usage: {
305
- inputTokens: 1000,
306
- outputTokens: 500,
307
- totalTokens: 1500,
308
- cachedInputTokens: 200,
309
- },
310
- metadata: {
311
- anthropic: {},
312
- },
313
- })
314
-
315
- expect(result.tokens.input).toBe(1000)
316
- expect(result.tokens.cache.read).toBe(200)
317
- })
318
-
319
- test("handles reasoning tokens", () => {
320
- const model = createModel({ context: 100_000, output: 32_000 })
321
- const result = Session.getUsage({
322
- model,
323
- usage: {
324
- inputTokens: 1000,
325
- outputTokens: 500,
326
- totalTokens: 1500,
327
- reasoningTokens: 100,
328
- },
329
- })
330
-
331
- expect(result.tokens.reasoning).toBe(100)
332
- })
333
-
334
- test("handles undefined optional values gracefully", () => {
335
- const model = createModel({ context: 100_000, output: 32_000 })
336
- const result = Session.getUsage({
337
- model,
338
- usage: {
339
- inputTokens: 0,
340
- outputTokens: 0,
341
- totalTokens: 0,
342
- },
343
- })
344
-
345
- expect(result.tokens.input).toBe(0)
346
- expect(result.tokens.output).toBe(0)
347
- expect(result.tokens.reasoning).toBe(0)
348
- expect(result.tokens.cache.read).toBe(0)
349
- expect(result.tokens.cache.write).toBe(0)
350
- expect(Number.isNaN(result.cost)).toBe(false)
351
- })
352
-
353
- test("calculates cost correctly", () => {
354
- const model = createModel({
355
- context: 100_000,
356
- output: 32_000,
357
- cost: {
358
- input: 3,
359
- output: 15,
360
- cache: { read: 0.3, write: 3.75 },
361
- },
362
- })
363
- const result = Session.getUsage({
364
- model,
365
- usage: {
366
- inputTokens: 1_000_000,
367
- outputTokens: 100_000,
368
- totalTokens: 1_100_000,
369
- },
370
- })
371
-
372
- expect(result.cost).toBe(3 + 1.5)
373
- })
374
-
375
- test.each(["@ai-sdk/anthropic", "@ai-sdk/amazon-bedrock", "@ai-sdk/google-vertex/anthropic"])(
376
- "computes total from components for %s models",
377
- (npm) => {
378
- const model = createModel({ context: 100_000, output: 32_000, npm })
379
- const usage = {
380
- inputTokens: 1000,
381
- outputTokens: 500,
382
- // These providers typically report total as input + output only,
383
- // excluding cache read/write.
384
- totalTokens: 1500,
385
- cachedInputTokens: 200,
386
- }
387
- if (npm === "@ai-sdk/amazon-bedrock") {
388
- const result = Session.getUsage({
389
- model,
390
- usage,
391
- metadata: {
392
- bedrock: {
393
- usage: {
394
- cacheWriteInputTokens: 300,
395
- },
396
- },
397
- },
398
- })
399
-
400
- expect(result.tokens.input).toBe(1000)
401
- expect(result.tokens.cache.read).toBe(200)
402
- expect(result.tokens.cache.write).toBe(300)
403
- expect(result.tokens.total).toBe(2000)
404
- return
405
- }
406
-
407
- const result = Session.getUsage({
408
- model,
409
- usage,
410
- metadata: {
411
- anthropic: {
412
- cacheCreationInputTokens: 300,
413
- },
414
- },
415
- })
416
-
417
- expect(result.tokens.input).toBe(1000)
418
- expect(result.tokens.cache.read).toBe(200)
419
- expect(result.tokens.cache.write).toBe(300)
420
- expect(result.tokens.total).toBe(2000)
421
- },
422
- )
423
- })
@@ -1,170 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import path from "path"
3
- import { InstructionPrompt } from "../../src/session/instruction"
4
- import { Instance } from "../../src/project/instance"
5
- import { Global } from "../../src/global"
6
- import { tmpdir } from "../fixture/fixture"
7
-
8
- describe("InstructionPrompt.resolve", () => {
9
- test("returns empty when AGENTS.md is at project root (already in systemPaths)", async () => {
10
- await using tmp = await tmpdir({
11
- init: async (dir) => {
12
- await Bun.write(path.join(dir, "AGENTS.md"), "# Root Instructions")
13
- await Bun.write(path.join(dir, "src", "file.ts"), "const x = 1")
14
- },
15
- })
16
- await Instance.provide({
17
- directory: tmp.path,
18
- fn: async () => {
19
- const system = await InstructionPrompt.systemPaths()
20
- expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
21
-
22
- const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"), "test-message-1")
23
- expect(results).toEqual([])
24
- },
25
- })
26
- })
27
-
28
- test("returns AGENTS.md from subdirectory (not in systemPaths)", async () => {
29
- await using tmp = await tmpdir({
30
- init: async (dir) => {
31
- await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
32
- await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
33
- },
34
- })
35
- await Instance.provide({
36
- directory: tmp.path,
37
- fn: async () => {
38
- const system = await InstructionPrompt.systemPaths()
39
- expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
40
-
41
- const results = await InstructionPrompt.resolve(
42
- [],
43
- path.join(tmp.path, "subdir", "nested", "file.ts"),
44
- "test-message-2",
45
- )
46
- expect(results.length).toBe(1)
47
- expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
48
- },
49
- })
50
- })
51
-
52
- test("doesn't reload AGENTS.md when reading it directly", async () => {
53
- await using tmp = await tmpdir({
54
- init: async (dir) => {
55
- await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
56
- await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
57
- },
58
- })
59
- await Instance.provide({
60
- directory: tmp.path,
61
- fn: async () => {
62
- const filepath = path.join(tmp.path, "subdir", "AGENTS.md")
63
- const system = await InstructionPrompt.systemPaths()
64
- expect(system.has(filepath)).toBe(false)
65
-
66
- const results = await InstructionPrompt.resolve([], filepath, "test-message-2")
67
- expect(results).toEqual([])
68
- },
69
- })
70
- })
71
- })
72
-
73
- describe("InstructionPrompt.systemPaths OPENCODE_CONFIG_DIR", () => {
74
- let originalConfigDir: string | undefined
75
-
76
- beforeEach(() => {
77
- originalConfigDir = process.env["OPENCODE_CONFIG_DIR"]
78
- })
79
-
80
- afterEach(() => {
81
- if (originalConfigDir === undefined) {
82
- delete process.env["OPENCODE_CONFIG_DIR"]
83
- } else {
84
- process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir
85
- }
86
- })
87
-
88
- test("prefers OPENCODE_CONFIG_DIR AGENTS.md over global when both exist", async () => {
89
- await using profileTmp = await tmpdir({
90
- init: async (dir) => {
91
- await Bun.write(path.join(dir, "AGENTS.md"), "# Profile Instructions")
92
- },
93
- })
94
- await using globalTmp = await tmpdir({
95
- init: async (dir) => {
96
- await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
97
- },
98
- })
99
- await using projectTmp = await tmpdir()
100
-
101
- process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
102
- const originalGlobalConfig = Global.Path.config
103
- ;(Global.Path as { config: string }).config = globalTmp.path
104
-
105
- try {
106
- await Instance.provide({
107
- directory: projectTmp.path,
108
- fn: async () => {
109
- const paths = await InstructionPrompt.systemPaths()
110
- expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(true)
111
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(false)
112
- },
113
- })
114
- } finally {
115
- ;(Global.Path as { config: string }).config = originalGlobalConfig
116
- }
117
- })
118
-
119
- test("falls back to global AGENTS.md when OPENCODE_CONFIG_DIR has no AGENTS.md", async () => {
120
- await using profileTmp = await tmpdir()
121
- await using globalTmp = await tmpdir({
122
- init: async (dir) => {
123
- await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
124
- },
125
- })
126
- await using projectTmp = await tmpdir()
127
-
128
- process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
129
- const originalGlobalConfig = Global.Path.config
130
- ;(Global.Path as { config: string }).config = globalTmp.path
131
-
132
- try {
133
- await Instance.provide({
134
- directory: projectTmp.path,
135
- fn: async () => {
136
- const paths = await InstructionPrompt.systemPaths()
137
- expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(false)
138
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
139
- },
140
- })
141
- } finally {
142
- ;(Global.Path as { config: string }).config = originalGlobalConfig
143
- }
144
- })
145
-
146
- test("uses global AGENTS.md when OPENCODE_CONFIG_DIR is not set", async () => {
147
- await using globalTmp = await tmpdir({
148
- init: async (dir) => {
149
- await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
150
- },
151
- })
152
- await using projectTmp = await tmpdir()
153
-
154
- delete process.env["OPENCODE_CONFIG_DIR"]
155
- const originalGlobalConfig = Global.Path.config
156
- ;(Global.Path as { config: string }).config = globalTmp.path
157
-
158
- try {
159
- await Instance.provide({
160
- directory: projectTmp.path,
161
- fn: async () => {
162
- const paths = await InstructionPrompt.systemPaths()
163
- expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
164
- },
165
- })
166
- } finally {
167
- ;(Global.Path as { config: string }).config = originalGlobalConfig
168
- }
169
- })
170
- })