diff --git a/src/daemon/context-windows.test.ts b/src/daemon/context-windows.test.ts index 0ca6ebc..b1f2e79 100644 --- a/src/daemon/context-windows.test.ts +++ b/src/daemon/context-windows.test.ts @@ -35,6 +35,23 @@ describe("contextWindowForModel", () => { expect(contextWindowForModel("claude-sonnet-4-5-1m")).toBe(ONE_MILLION_CONTEXT); }); + test("bracket [1m] form -> 1M (the form Claude Code actually emits)", () => { + // Regression: these resolved to 200k, so asking EXPLICITLY for the 1M + // variant sized the window WORSE than the bare `opus` alias — observed on + // a live session running `opus[1m]`, which reported ~500% occupancy and a + // 5x-too-small fork seed budget. + expect(contextWindowForModel("opus[1m]")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("sonnet[1m]")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-opus-5[1m]")).toBe(ONE_MILLION_CONTEXT); + // Case-insensitive, like every other branch. + expect(contextWindowForModel("OPUS[1M]")).toBe(ONE_MILLION_CONTEXT); + }); + + test("haiku stays 200k in bracket form too", () => { + // haiku has no 1M variant; a bracket suffix must not manufacture one. + expect(contextWindowForModel("haiku")).toBe(DEFAULT_CONTEXT_WINDOW); + }); + test("unknown claude model -> conservative 200k miss", () => { expect(contextWindowForModel("claude-sonnet-4-0")).toBe(DEFAULT_CONTEXT_WINDOW); expect(contextWindowForModel("custom-model")).toBe(DEFAULT_CONTEXT_WINDOW); diff --git a/src/daemon/context-windows.ts b/src/daemon/context-windows.ts index fea9cc3..33b7527 100644 --- a/src/daemon/context-windows.ts +++ b/src/daemon/context-windows.ts @@ -51,7 +51,14 @@ export function contextWindowForModel(modelId: string | undefined | null): numbe for (const family of ONE_MILLION_FAMILIES) { if (m.includes(family)) return ONE_MILLION_CONTEXT; } - if (m.includes("-1m")) return ONE_MILLION_CONTEXT; + // The 1M variant appears in two forms: the suffix on a full model id + // (`claude-opus-4-5-1m`) and the BRACKET form Claude Code uses on aliases + // and ids alike (`opus[1m]`, `claude-opus-5[1m]`). Matching only the former + // meant an EXPLICIT 1M request resolved to 200k while the bare `opus` alias + // correctly resolved to 1M — inverting the caller's intent, and under-sizing + // the window that drives the percent-of-window display, the fork seed budget + // (seedBudgetChars), and auto-rotate occupancy. + if (m.includes("-1m") || m.includes("[1m]")) return ONE_MILLION_CONTEXT; // Aliases (matching the daemon's model resolver: opus → Opus 4.8, // sonnet → Sonnet 5 — both 1M; haiku → Haiku 4.5 at 200k).