Skip to content

Commit 3ebd601

Browse files
committed
fix(model): stop opencode auto-compacting Cursor sessions
The Cursor agent runtime compacts its own conversation as it approaches its context threshold (`preCompact` hook, `trigger: "auto"`), so a second opencode-driven pass is redundant. It is also actively harmful, in two ways. The compaction turn asks the model to summarize with zero tools declared. The Cursor agent runs its own tools regardless, and opencode rejects the result outright: Tool call not allowed while generating summary: <tool> Worse, compaction rewrites the transcript, so the next turn no longer matches what the Cursor agent saw. `classifyTurn` correctly reports a divergence and a fresh Cursor agent is created — and every distinct agentId permanently holds a guarded SQLite store.db/-wal/-shm triple. `SDKAgent.close()` cannot release those; it only flushes analytics and releases the executor lease, while the checkpoint store is cached in an agentId-keyed map evicted solely by dispose()/deleteAgent(). That descriptor growth fed an uncatchable EXC_GUARD kill of the whole opencode process (guard cookie 0x08fd4dbfade2dead — Apple's SQLite guard). Suppress the trigger with a large `limit.input`, which is the value opencode uses as its compaction threshold: Is(e) = limit.input ? limit.input - reserved : limit.context - maxOutput `limit.context` is left honest, so the TUI context gauge and cost reporting keep working — the alternative lever, `limit.context: 0`, would disable the trigger but blank the gauge and regress #89. `limit.input` is honored by opencode's runtime but is not declared in the published @opencode-ai/sdk config types, so it is excluded from `_limitKeyGuard` (which still protects context/output) and gated instead by a new assertion in the integration test: without it, opencode dropping support would silently restore auto-compaction and the fd leak. Verified against the opencode 1.18.11 binary by enumerating the call sites of Is() rather than textual hits on limit.input, since consumers reach it transitively. The only other consumer, Pd() (preserve-recent tokens, also used by manual /compact), clamps to 8000 both before and after. Confirmed end-to-end under an isolated HOME that the sentinel survives config validation into Provider.list() with limit.context intact. Manual /compact is unaffected. Opt back out with `provider.cursor.options.autoCompaction: true`. Tradeoff, documented in the README: this suppresses the proactive threshold only, and opencode has no reactive context-overflow recovery wired up for this provider, so its transcript is no longer trimmed automatically. Ordinary turns send just the new message, but a cold replay resends everything; if that overflows, the turn fails and /compact is the manual recovery.
1 parent 7c3605e commit 3ebd601

10 files changed

Lines changed: 280 additions & 18 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
- **opencode's threshold-triggered auto-compaction is now suppressed for Cursor models by
8+
default.** The Cursor agent runtime already self-compacts on its own context threshold
9+
(`preCompact` hook with `trigger: "auto"`), so opencode-driven compaction was redundant —
10+
and it caused two real failures. First, the compaction turn runs with zero tools declared
11+
while the Cursor agent uses its own tools anyway, which opencode rejects (`Tool call not
12+
allowed while generating summary`) — mitigated in 0.7.1-next.1 (#91), and now avoided
13+
entirely for the automatic trigger. Second, compaction rewrites the transcript, which
14+
classifies as a divergence and mints a **fresh Cursor agentId** — and every distinct
15+
agentId permanently holds a guarded SQLite `store.db`/`-wal`/`-shm` triple that
16+
`agent.close()` cannot release (it only flushes analytics and releases the executor lease).
17+
That descriptor growth fed an uncatchable `EXC_GUARD` process kill.
18+
19+
Suppression uses a large `limit.input` — the value opencode uses as its compaction
20+
threshold — leaving the real `limit.context` intact so the TUI context gauge and cost
21+
reporting still work. Manual `/compact` is unaffected and still relies on #91's fix.
22+
23+
**Tradeoff:** this suppresses the proactive threshold trigger only, and opencode has no
24+
reactive context-overflow recovery wired up for this provider, so its transcript is no
25+
longer trimmed automatically. Ordinary turns send only the new message, but a cold replay
26+
(new session, expired agent, changed MCP set) resends everything; if that overflows the
27+
model the turn fails and `/compact` is the manual recovery. Opt back out with
28+
`provider.cursor.options.autoCompaction: true`.
29+
730
## [0.7.1-next.1] — 2026-08-03 (pre-release)
831

932
Pre-release of the compaction fix (#91). Not yet on `latest`; install with

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ See [SECURITY.md](./SECURITY.md) for the full threat model.
187187
| `toolDisplay` | `"blocks"` | How Cursor's internal tool activity is shown — see [Tool display](#tool-display) |
188188
| `systemPrompt` | `"rules"` | How opencode's system prompt reaches the agent — see [System prompt](#system-prompt) |
189189
| `transport` || Cursor agent transport (`"http1"` \| `"http2-direct"` \| `"sidecar"`) — see [Transport](#transport) |
190+
| `autoCompaction` | `false` | Let opencode drive auto-compaction. Off by default because the Cursor agent self-compacts — see [Compaction](#compaction) |
190191

191192
| Environment variable | Default | Meaning |
192193
| --- | --- | --- |
@@ -423,6 +424,42 @@ To force the fallback:
423424
{ "provider": { "cursor": { "options": { "toolDisplay": "reasoning" } } } }
424425
```
425426

427+
## Compaction
428+
429+
**opencode's threshold-triggered auto-compaction is suppressed for Cursor models by default.** The
430+
Cursor agent runtime compacts its own conversation as it approaches its context threshold, so a
431+
second, opencode-driven pass is redundant — and it is actively harmful here:
432+
433+
- The compaction turn asks the model to summarize with **no tools available**. The Cursor agent runs
434+
its own tools regardless, which opencode rejects outright
435+
(`Tool call not allowed while generating summary`).
436+
- Compaction rewrites the transcript, so the next turn no longer matches what the Cursor agent saw.
437+
The plugin correctly treats that as a divergence and creates a **fresh Cursor agent** — and every
438+
distinct agent permanently holds its own SQLite store open for the life of the process, which has
439+
been observed to crash opencode outright.
440+
441+
The suppression works by emitting a very large `limit.input`, which is what opencode uses as its
442+
compaction threshold. The real `limit.context` is left untouched, so the TUI's context-window gauge
443+
and cost reporting keep working.
444+
445+
> [!IMPORTANT]
446+
> This suppresses the **proactive** threshold trigger only, and opencode has no reactive
447+
> context-overflow recovery wired up for this provider. In exchange, opencode's transcript is no
448+
> longer trimmed automatically, so it grows for the life of the session. Ordinary turns send only
449+
> the new message to an already-running agent, but a *cold replay* — a new session, an expired
450+
> agent, or a changed MCP server set — resends the whole transcript. If that ever overflows the
451+
> model, the turn fails with a provider error and the fix is to run `/compact` manually.
452+
>
453+
> Set `autoCompaction: true` if you would rather have opencode keep bounding the transcript for you.
454+
455+
Manual `/compact` is unaffected and still works — it has no threshold gate.
456+
457+
To hand compaction back to opencode:
458+
459+
```json
460+
{ "provider": { "cursor": { "options": { "autoCompaction": true } } } }
461+
```
462+
426463
## Transport
427464

428465
opencode runs on [Bun](https://bun.sh), whose `node:http2` client is incompatible with the Cursor

scripts/integration-test.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ fi
7373

7474
echo "PASS: opencode loaded the plugin and listed $CURSOR_COUNT Cursor model(s)."
7575

76+
# Drift gate for the auto-compaction suppression. We disable opencode's
77+
# threshold-triggered compaction by emitting a large `limit.input`, which is what
78+
# opencode uses as that threshold. That field is honored by opencode's runtime
79+
# but is NOT declared in the published @opencode-ai/sdk config types, so nothing
80+
# in `tsc` or the unit suite can notice if opencode ever stops reading it.
81+
# Without this check, such a regression is silent: auto-compaction quietly
82+
# resumes, and with it the per-compaction agentId churn that leaks guarded
83+
# SQLite descriptors and has crashed opencode outright.
84+
VERBOSE_OUT="$("$OPENCODE" models cursor --verbose 2>/dev/null)"
85+
if ! printf '%s\n' "$VERBOSE_OUT" | grep -q '"input": 1000000000'; then
86+
echo "FAIL: limit.input sentinel did not survive into opencode's model registry."
87+
echo " Auto-compaction suppression is broken — see 'Compaction' in README.md."
88+
echo "----- limit blocks as resolved by opencode -----"
89+
printf '%s\n' "$VERBOSE_OUT" | grep -A4 '"limit"' | head -20
90+
exit 1
91+
fi
92+
# The gauge must still work: context has to stay a real value, not be zeroed.
93+
if printf '%s\n' "$VERBOSE_OUT" | grep -A4 '"limit"' | grep -q '"context": 0'; then
94+
echo "FAIL: limit.context was zeroed — the TUI context gauge would be dead."
95+
exit 1
96+
fi
97+
echo "PASS: limit.input sentinel reaches opencode's registry with limit.context intact."
98+
7699
# Assert the packed artifact actually ships the delegation tools.
77100
PLUGIN_JS="$WORK/node_modules/@stablekernel/opencode-cursor/dist/plugin/index.js"
78101
for TOOL in cursor_cloud_agent cursor_delegate; do

src/model-discovery.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { ModelListItem } from "@cursor/sdk";
22
import type { Config } from "@opencode-ai/plugin";
33
import { fingerprintApiKey, resolveCursorApiKey } from "./api-key.js";
4-
import { resolveContextLimit, resolveCost, resolveOutputLimit } from "./model-limits.js";
4+
import {
5+
NO_AUTO_COMPACTION_INPUT_LIMIT,
6+
resolveContextLimit,
7+
resolveCost,
8+
resolveOutputLimit,
9+
} from "./model-limits.js";
510
import { readLatestModelCache, readModelCache, writeModelCache } from "./model-cache.js";
611
import { FALLBACK_MODELS } from "./fallback-models.js";
712
import { loadCursorSdk } from "./cursor-runtime.js";
@@ -111,9 +116,17 @@ export interface OpencodeModelConfigEntry {
111116
* Per-model context/output window. opencode's config channel is the only
112117
* one that reaches the model registry for providers absent from
113118
* models.dev, so the TUI session header's context-window percentage
114-
* depends on this being present. Both fields are required by the schema.
119+
* depends on this being present. `context` and `output` are required by
120+
* the schema.
121+
*
122+
* `input` is an undocumented-but-runtime-honored field used only as
123+
* opencode's auto-compaction threshold. We emit
124+
* {@link NO_AUTO_COMPACTION_INPUT_LIMIT} to suppress auto-compaction while
125+
* keeping `context` honest so the TUI gauge still works. The published
126+
* `@opencode-ai/sdk` config types omit it, so it is excluded from
127+
* `_limitKeyGuard` below.
115128
*/
116-
limit: { context: number; output: number };
129+
limit: { context: number; input?: number; output: number };
117130
/**
118131
* Per-model API pricing, USD per million tokens. Note the FLAT snake_case
119132
* cache keys — the config schema (`ProviderConfig` in
@@ -154,8 +167,11 @@ const _costKeyGuard: _KeysAccepted<
154167
NonNullable<AcceptedModelConfig["cost"]>
155168
> = true;
156169
void _costKeyGuard;
170+
// `input` is deliberately excluded: opencode's runtime reads it (verified in
171+
// the 1.18.11 binary and end-to-end via `Provider.list()`), but the published
172+
// config types don't declare it. The guard still protects `context`/`output`.
157173
const _limitKeyGuard: _KeysAccepted<
158-
OpencodeModelConfigEntry["limit"],
174+
Omit<OpencodeModelConfigEntry["limit"], "input">,
159175
NonNullable<AcceptedModelConfig["limit"]>
160176
> = true;
161177
void _limitKeyGuard;
@@ -165,7 +181,10 @@ void _limitKeyGuard;
165181
* Cursor SDK runs an agent (it calls tools itself), so every model is marked
166182
* `tool_call: true` and `temperature: false`.
167183
*/
168-
export function toOpencodeModels(items: ModelListItem[]): Record<string, OpencodeModelConfigEntry> {
184+
export function toOpencodeModels(
185+
items: ModelListItem[],
186+
opts: { autoCompaction?: boolean } = {},
187+
): Record<string, OpencodeModelConfigEntry> {
169188
const out: Record<string, OpencodeModelConfigEntry> = {};
170189
for (const item of items) {
171190
const params = defaultModelParams(item);
@@ -181,6 +200,12 @@ export function toOpencodeModels(items: ModelListItem[]): Record<string, Opencod
181200
options: Object.keys(params).length > 0 ? { params } : {},
182201
limit: {
183202
context: resolveContextLimit(item.id),
203+
// Suppress opencode's auto-compaction unless the user opts in: the
204+
// Cursor agent self-compacts, and opencode's compaction mints a
205+
// fresh agentId per cycle, permanently leaking guarded SQLite fds.
206+
...(opts.autoCompaction
207+
? {}
208+
: { input: NO_AUTO_COMPACTION_INPUT_LIMIT }),
184209
output: resolveOutputLimit(item.id),
185210
},
186211
cost: {

src/model-limits.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,41 @@ const MODEL_CONTEXT_LIMITS: Record<string, number> = {
7474

7575
const DEFAULT_CONTEXT_LIMIT = 200_000;
7676

77+
/**
78+
* Sentinel `limit.input` that pushes opencode's auto-compaction threshold out
79+
* of reach, so auto-compaction never fires. opencode computes the threshold as
80+
* `limit.input ? limit.input - reserved : limit.context - maxOutput`, so a huge
81+
* `input` makes it unreachable while `limit.context` stays honest — the TUI
82+
* context gauge keeps working.
83+
*
84+
* Why suppress it: the Cursor agent runtime self-compacts on its own context
85+
* threshold (`@cursor/sdk` `dist/esm/357.js`, `preCompact` hook with
86+
* `trigger: "auto"`), so opencode-driven compaction is redundant. It is also
87+
* harmful — each opencode compaction rewrites the transcript, which classifies
88+
* as `divergence` and mints a fresh Cursor agentId, and every distinct agentId
89+
* permanently adds a guarded SQLite `store.db`/`-wal`/`-shm` triple that
90+
* `agent.close()` cannot release.
91+
*
92+
* This is NOT a real model capability. Verified against the opencode 1.18.11
93+
* binary by enumerating the call sites of `Is()` (the threshold function) rather
94+
* than textual hits on `limit.input`, since consumers reach it transitively:
95+
* - `vl()` — the proactive auto-compaction trigger. Suppressed here.
96+
* - `Pd()` — preserve-recent-tokens budget, also used by manual
97+
* `/compact`. Inert: it is `min(8000, max(2000,
98+
* floor(Is*0.25)))`, which saturates at 8000 for any
99+
* `Is >= 32000` — true both before and after the sentinel.
100+
* Everything else that touches `limit.input` is catalog merge/serialization.
101+
*
102+
* Also verified end-to-end (isolated HOME, `opencode models cursor --verbose`)
103+
* that a config-channel `limit.input` survives validation and reaches
104+
* `Provider.list()` with `limit.context` intact.
105+
*
106+
* Caveat: `Is()` is `max(0, input - reserved)`, so a user setting
107+
* `compaction.reserved >= this value` would drive the threshold to 0 and make
108+
* compaction fire every turn. Absurd but user-settable.
109+
*/
110+
export const NO_AUTO_COMPACTION_INPUT_LIMIT = 1_000_000_000;
111+
77112
/**
78113
* Resolve a model's context window by longest-prefix match against
79114
* {@link MODEL_CONTEXT_LIMITS}. Falls back to 200K for unknown models.

src/plugin/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ export const CursorPlugin: Plugin = async (input) => {
126126
let resolvedCwd = directory ?? process.cwd();
127127
let forwardMcp = true;
128128
let userMcp: Record<string, McpServerConfig> = {};
129+
// Whether to let opencode drive auto-compaction. Default false: the Cursor
130+
// agent self-compacts (preCompact hook, trigger:"auto"), so opencode's
131+
// compaction is redundant and is what mints a fresh agentId per compaction.
132+
// Opt in with `provider.cursor.options.autoCompaction: true`.
133+
let autoCompaction = false;
129134
// Skill forwarding state, mirroring the MCP forwarding pattern.
130135
let forwardSkills = true;
131136
let skillFilterOptions: SkillFilterOptions | undefined;
@@ -183,6 +188,7 @@ export const CursorPlugin: Plugin = async (input) => {
183188
// Forward opencode's configured MCP servers to the Cursor
184189
// agent so it can use the same servers. Opt out via
185190
// `provider.cursor.options.forwardMcp: false`.
191+
autoCompaction = existingOptions["autoCompaction"] === true;
186192
forwardMcp = existingOptions["forwardMcp"] !== false;
187193
userMcp = (existingOptions["mcpServers"] ?? {}) as Record<
188194
string,
@@ -265,7 +271,10 @@ export const CursorPlugin: Plugin = async (input) => {
265271
? { skillsCatalogue: currentSkillsCatalogue }
266272
: {}),
267273
},
268-
models: { ...toOpencodeModels(models), ...(existing.models ?? {}) },
274+
models: {
275+
...toOpencodeModels(models, { autoCompaction }),
276+
...(existing.models ?? {}),
277+
},
269278
};
270279
},
271280

@@ -274,7 +283,7 @@ export const CursorPlugin: Plugin = async (input) => {
274283
models: async (_provider, ctx) => {
275284
const apiKey = apiKeyFromAuth(ctx.auth);
276285
const { models } = await discoverModels({ apiKey });
277-
return buildModelV2Map(models);
286+
return buildModelV2Map(models, { autoCompaction });
278287
},
279288
},
280289

src/plugin/model-v2.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { Model as ModelV2 } from "@opencode-ai/sdk/v2";
22
import type { ModelListItem } from "@cursor/sdk";
33
import { modelSupportsReasoning } from "../model-discovery.js";
4-
import { resolveContextLimit, resolveCost, resolveOutputLimit } from "../model-limits.js";
4+
import {
5+
NO_AUTO_COMPACTION_INPUT_LIMIT,
6+
resolveContextLimit,
7+
resolveCost,
8+
resolveOutputLimit,
9+
} from "../model-limits.js";
510
import { buildModelVariants, defaultModelParams } from "../model-variants.js";
611

712
export const PROVIDER_ID = "cursor";
@@ -24,7 +29,10 @@ export function providerNpm(): string {
2429
* limits are resolved per model from the shared maps in `../model-limits.js`,
2530
* falling back to $0 / 200K context / 32K output for models absent from them.
2631
*/
27-
export function buildModelV2Map(items: ModelListItem[]): Record<string, ModelV2> {
32+
export function buildModelV2Map(
33+
items: ModelListItem[],
34+
opts: { autoCompaction?: boolean } = {},
35+
): Record<string, ModelV2> {
2836
const out: Record<string, ModelV2> = {};
2937
for (const item of items) {
3038
const params = defaultModelParams(item);
@@ -46,7 +54,13 @@ export function buildModelV2Map(items: ModelListItem[]): Record<string, ModelV2>
4654
const c = resolveCost(item.id);
4755
return { input: c.input, output: c.output, cache: { read: c.cacheRead, write: c.cacheWrite } };
4856
})(),
49-
limit: { context: resolveContextLimit(item.id), output: resolveOutputLimit(item.id) },
57+
limit: {
58+
context: resolveContextLimit(item.id),
59+
...(opts.autoCompaction
60+
? {}
61+
: { input: NO_AUTO_COMPACTION_INPUT_LIMIT }),
62+
output: resolveOutputLimit(item.id),
63+
},
5064
status: "active",
5165
options: Object.keys(params).length > 0 ? { params } : {},
5266
headers: {},

test/model-discovery.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ vi.mock("../src/model-cache.js", () => ({
1111
const { discoverModels, modelSupportsReasoning, toOpencodeModels } = await import(
1212
"../src/model-discovery.js"
1313
);
14+
const { NO_AUTO_COMPACTION_INPUT_LIMIT } = await import("../src/model-limits.js");
1415

1516
afterEach(() => readLatestModelCache.mockReset());
1617

@@ -83,16 +84,30 @@ describe("toOpencodeModels", () => {
8384

8485
describe("toOpencodeModels config-channel limits and cost", () => {
8586
it("emits per-model limit with both context and output", () => {
86-
const out = toOpencodeModels([
87-
{ id: "claude-opus-4-8", displayName: "Opus 4.8" },
88-
{ id: "gpt-5.5", displayName: "GPT-5.5" },
89-
{ id: "grok-4.5", displayName: "Grok 4.5" },
90-
] satisfies ModelListItem[]);
87+
const out = toOpencodeModels(
88+
[
89+
{ id: "claude-opus-4-8", displayName: "Opus 4.8" },
90+
{ id: "gpt-5.5", displayName: "GPT-5.5" },
91+
{ id: "grok-4.5", displayName: "Grok 4.5" },
92+
] satisfies ModelListItem[],
93+
{ autoCompaction: true },
94+
);
9195
expect(out["claude-opus-4-8"]!.limit).toEqual({ context: 300_000, output: 64_000 });
9296
expect(out["gpt-5.5"]!.limit).toEqual({ context: 272_000, output: 64_000 });
9397
expect(out["grok-4.5"]!.limit).toEqual({ context: 256_000, output: 32_000 });
9498
});
9599

100+
it("emits the no-auto-compaction input sentinel by default, keeping context honest", () => {
101+
const out = toOpencodeModels([
102+
{ id: "claude-opus-4-8", displayName: "Opus 4.8" },
103+
] satisfies ModelListItem[]);
104+
expect(out["claude-opus-4-8"]!.limit).toEqual({
105+
context: 300_000,
106+
input: NO_AUTO_COMPACTION_INPUT_LIMIT,
107+
output: 64_000,
108+
});
109+
});
110+
96111
it("emits cost with FLAT snake_case cache keys, not nested cache object", () => {
97112
const out = toOpencodeModels([
98113
{ id: "claude-sonnet-4-6", displayName: "Sonnet 4.6" },
@@ -119,10 +134,12 @@ describe("toOpencodeModels config-channel limits and cost", () => {
119134
});
120135

121136
it("falls back to 200K/32K and $0 for unknown models", () => {
122-
const out = toOpencodeModels([
123-
{ id: "brand-new-model", displayName: "New" },
124-
] satisfies ModelListItem[]);
137+
const out = toOpencodeModels(
138+
[{ id: "brand-new-model", displayName: "New" }] satisfies ModelListItem[],
139+
{ autoCompaction: true },
140+
);
125141
expect(out["brand-new-model"]!.limit).toEqual({ context: 200_000, output: 32_000 });
142+
expect(out["brand-new-model"]!.limit.input).toBeUndefined();
126143
expect(out["brand-new-model"]!.cost).toEqual({
127144
input: 0,
128145
output: 0,

test/model-v2.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22
import type { ModelListItem } from "@cursor/sdk";
33
import { buildModelV2Map } from "../src/plugin/model-v2.js";
4+
import { NO_AUTO_COMPACTION_INPUT_LIMIT } from "../src/model-limits.js";
45

56
describe("buildModelV2Map", () => {
67
it("seeds the fast-off default into options and exposes a fast opt-in variant", () => {
@@ -47,6 +48,24 @@ describe("buildModelV2Map", () => {
4748
expect(map["claude-opus-4-8"]!.limit.context).toBe(300_000);
4849
});
4950

51+
it("emits the no-auto-compaction input sentinel by default, keeping context honest", () => {
52+
// The Cursor agent self-compacts; opencode's compaction mints a fresh
53+
// agentId per cycle, which permanently leaks guarded SQLite descriptors.
54+
const map = buildModelV2Map([{ id: "claude-opus-4-8", displayName: "Opus 4.8" }]);
55+
expect(map["claude-opus-4-8"]!.limit.input).toBe(NO_AUTO_COMPACTION_INPUT_LIMIT);
56+
// context stays real so the TUI gauge keeps working
57+
expect(map["claude-opus-4-8"]!.limit.context).toBe(300_000);
58+
expect(map["claude-opus-4-8"]!.limit.output).toBe(64_000);
59+
});
60+
61+
it("omits the input sentinel when autoCompaction is opted in", () => {
62+
const map = buildModelV2Map([{ id: "claude-opus-4-8", displayName: "Opus 4.8" }], {
63+
autoCompaction: true,
64+
});
65+
expect(map["claude-opus-4-8"]!.limit.input).toBeUndefined();
66+
expect(map["claude-opus-4-8"]!.limit.context).toBe(300_000);
67+
});
68+
5069
it("sets cost from per-model map for known models", () => {
5170
const map = buildModelV2Map([
5271
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },

0 commit comments

Comments
 (0)