diff --git a/devlog/_plan/260916_cursor_http2_toolcall/000_plan.md b/devlog/_plan/260916_cursor_http2_toolcall/000_plan.md index 0b546f62d2..9c9b73e295 100644 --- a/devlog/_plan/260916_cursor_http2_toolcall/000_plan.md +++ b/devlog/_plan/260916_cursor_http2_toolcall/000_plan.md @@ -18,6 +18,9 @@ renamed the display alias. That leak is the first stacked PR. Observed - Stop: both PRs exist with parent/child bases. - Memory artifact: this directory. - Terminal: DONE (PRs opened) / BLOCKED (push/template) / UNSAFE (exec default-on). +- Shipped (2026-09-16, `gh pr view` bases): + - L1 https://github.com/lidge-jun/opencodex/pull/4815 `dev` ← `cursor/l1-text-toolcall-quarantine` + - L2 https://github.com/lidge-jun/opencodex/pull/4816 `cursor/l1-text-toolcall-quarantine` ← `cursor/l2-observed-max-tokens` - Escalation: live `api2` vs `agentn.global.api5` host cutover. ## Work-phase map diff --git a/devlog/_plan/260916_cursor_http2_toolcall/010_phase1_text_toolcall_quarantine.md b/devlog/_plan/260916_cursor_http2_toolcall/010_phase1_text_toolcall_quarantine.md index d5278fc078..cebb6ca417 100644 --- a/devlog/_plan/260916_cursor_http2_toolcall/010_phase1_text_toolcall_quarantine.md +++ b/devlog/_plan/260916_cursor_http2_toolcall/010_phase1_text_toolcall_quarantine.md @@ -34,3 +34,8 @@ Host cutover, stall-resume, CLI spawn, new test-layout file. ## Verifier NOT RUN locally. Hosted `bun test tests/providers/cursor/cursor-protobuf-events.test.ts`. + +## Shipped + +https://github.com/lidge-jun/opencodex/pull/4815 +`dev` ← `cursor/l1-text-toolcall-quarantine` (`407bf3ce56`). diff --git a/devlog/_plan/260916_cursor_http2_toolcall/020_phase2_observed_max_tokens.md b/devlog/_plan/260916_cursor_http2_toolcall/020_phase2_observed_max_tokens.md index f3e8648e66..e1220b2c8d 100644 --- a/devlog/_plan/260916_cursor_http2_toolcall/020_phase2_observed_max_tokens.md +++ b/devlog/_plan/260916_cursor_http2_toolcall/020_phase2_observed_max_tokens.md @@ -24,6 +24,17 @@ client-version bump. - `cursorRequestSizeContext` feeds that window into the existing 0.5-window overflow vs 429 prior. +## Shipped + +Process-local map in `discovery.ts`; checkpoint records a positive +`maxTokens` when `wireModelId` is set from `live-transport.ts`. +`inferCursorContextWindow(modelId, observed?)` prefers explicit then +recorded then heuristic. `cursorRequestSizeContext` is unchanged except +the comment — it already calls `inferCursorContextWindow`. + +https://github.com/lidge-jun/opencodex/pull/4816 +`cursor/l1-text-toolcall-quarantine` ← `cursor/l2-observed-max-tokens` (`8763dee2d2`). + ## Accept - Checkpoint with `maxTokens: 32000` makes a 20-token request classify as 429 diff --git a/src/adapters/cursor.ts b/src/adapters/cursor.ts index 25a6cca582..867cca2bab 100644 --- a/src/adapters/cursor.ts +++ b/src/adapters/cursor.ts @@ -99,11 +99,20 @@ function safeCursorTransportError(err: unknown, sizeContext?: CursorSizeContext) * estimate over the outgoing text vs the model's context window. Only used to keep * SMALL requests on the 429 class — unknown/large stays on the overflow mapping. */ -function cursorRequestSizeContext(request: { modelId: string; system: string[]; messages: { content: string }[] }): CursorSizeContext { +function cursorRequestSizeContext(request: { + modelId: string; + _cursorIdentityScope?: string; + system: string[]; + messages: { content: string }[]; +}): CursorSizeContext { const text = [...request.system, ...request.messages.map(message => message.content)].join("\n"); return { estimatedInputTokens: estimateTokens(text, request.modelId), - contextWindow: inferCursorContextWindow(request.modelId), + // Prefers this identity scope's checkpoint `maxTokens` over the id heuristic + // so a plan-gated ceiling participates in the 0.5-window overflow vs 429 prior. + contextWindow: inferCursorContextWindow(request.modelId, { + identityScope: request._cursorIdentityScope, + }), }; } @@ -171,7 +180,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda } const inheritedCheckpointRef = _parsed._providerContinuation?.cursor?.checkpointRef; const previousConversationId = _parsed._cursorConversationId; - let request = createCursorRequest(_parsed); + let request = { + ...createCursorRequest(_parsed), + _cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local", + }; requestSizeContext = cursorRequestSizeContext(request); // The builder may derive a stable provider id from the client thread when Responses state // is unavailable. Rekey only existing state; there is nothing to migrate on a fresh turn, @@ -413,7 +425,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda const remintConversationId = (failedConversationId: string) => { lastTransport = undefined; _parsed._cursorConversationId = undefined; - const next = createCursorRequest(_parsed, { forceFreshConversation: true }); + const next = { + ...createCursorRequest(_parsed, { forceFreshConversation: true }), + _cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local", + }; rekeyContextUsage(failedConversationId, next.conversationId); _parsed._cursorConversationId = next.conversationId; // Persist recovery for store:false clients that send any stable Cursor thread owner, so diff --git a/src/adapters/cursor/discovery.ts b/src/adapters/cursor/discovery.ts index 87dd47e923..9224f9b3ec 100644 --- a/src/adapters/cursor/discovery.ts +++ b/src/adapters/cursor/discovery.ts @@ -24,8 +24,54 @@ const CONTEXT_272K = 272_000; const CONTEXT_262K = 262_144; const CONTEXT_256K = 256_000; const CONTEXT_200K = 200_000; +export const CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES = 2_048; -export function inferCursorContextWindow(modelId: string): number { +/** + * Process-local ceilings from `ConversationTokenDetails.maxTokens` on live + * checkpoints. Each observation belongs to the Cursor identity scope that + * produced it; plan-gated accounts sharing one proxy must not overwrite each + * other's overflow prior (senpi `cursor-context-limit`). + */ +const observedCursorContextWindows = new Map(); + +interface CursorContextWindowOptions { + identityScope?: string; + observed?: number; +} + +function normalizeObservedWindowKey(modelId: string, identityScope?: string): string { + return `${identityScope?.trim() || "local"}\0${modelId.trim().toLowerCase()}`; +} + +export function recordObservedCursorContextWindow( + modelId: string, + maxTokens: number | undefined, + options: Pick = {}, +): void { + if (!modelId.trim()) return; + if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens) || maxTokens <= 0) return; + const key = normalizeObservedWindowKey(modelId, options.identityScope); + observedCursorContextWindows.delete(key); + observedCursorContextWindows.set(key, Math.floor(maxTokens)); + while (observedCursorContextWindows.size > CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES) { + const oldest = observedCursorContextWindows.keys().next().value; + if (oldest === undefined) break; + observedCursorContextWindows.delete(oldest); + } +} + +export function observedCursorContextWindow( + modelId: string, + options: Pick = {}, +): number | undefined { + return observedCursorContextWindows.get(normalizeObservedWindowKey(modelId, options.identityScope)); +} + +export function resetObservedCursorContextWindowsForTests(): void { + observedCursorContextWindows.clear(); +} + +function inferCursorContextWindowHeuristic(modelId: string): number { const id = modelId.trim().toLowerCase(); if (id.includes("1m")) return CONTEXT_1M; if (id.startsWith("gemini-")) return CONTEXT_1M; @@ -40,6 +86,24 @@ export function inferCursorContextWindow(modelId: string): number { return CURSOR_DEFAULT_CONTEXT_WINDOW; } +/** + * Infer a conservative context window for a Cursor model id. + * + * A positive explicit observation wins, then an identity-scoped process-local + * checkpoint `maxTokens`, then the id heuristic. Cursor's `AvailableModelsResponse` + * does not currently include per-model context window metadata. + */ +export function inferCursorContextWindow( + modelId: string, + options: CursorContextWindowOptions = {}, +): number { + const { observed } = options; + if (typeof observed === "number" && Number.isFinite(observed) && observed > 0) { + return Math.floor(observed); + } + return observedCursorContextWindow(modelId, options) ?? inferCursorContextWindowHeuristic(modelId); +} + function normalizeInputModalities(input: string[] | undefined): string[] { const values = (input ?? [...CURSOR_DEFAULT_INPUT_MODALITIES]) .map(item => item.trim()) diff --git a/src/adapters/cursor/live-transport.ts b/src/adapters/cursor/live-transport.ts index 49eeafbdc5..f5ad317d05 100644 --- a/src/adapters/cursor/live-transport.ts +++ b/src/adapters/cursor/live-transport.ts @@ -733,6 +733,8 @@ class LiveCursorTransport implements CursorTransport { syntheticStructuredEditToolNames, translatorBudget: this.translatorBudget, contextUsage, + wireModelId: request.modelId, + identityScope: request._cursorIdentityScope, ...(prepared.estimatedInputTokens !== undefined ? { estimatedInputTokens: prepared.estimatedInputTokens } : {}), diff --git a/src/adapters/cursor/protobuf-events.ts b/src/adapters/cursor/protobuf-events.ts index 9ee60f594f..1aa4f47eb0 100644 --- a/src/adapters/cursor/protobuf-events.ts +++ b/src/adapters/cursor/protobuf-events.ts @@ -20,6 +20,7 @@ import { type DrainedTextToolCall, type SuppressedTextToolCallScan, } from "./text-toolcall"; +import { recordObservedCursorContextWindow } from "./discovery"; import type { CursorServerMessage } from "./types"; import type { TranslatorBudget } from "../../lib/translator-budget"; @@ -193,6 +194,10 @@ export interface CursorProtobufEventState { sawRealClientToolCall?: boolean; /** Monotonic id suffix for tool calls promoted from text markers. */ textToolCallSeq?: number; + /** Wire model id used to record checkpoint `maxTokens` for the next turn. */ + wireModelId?: string; + /** Normalized Cursor identity scope that owns the observed checkpoint ceiling. */ + identityScope: string; } @@ -232,6 +237,10 @@ export function createCursorProtobufEventState(options: { */ estimatedInputTokens?: number; translatorBudget?: TranslatorBudget; + /** Wire model id for recording checkpoint `maxTokens` into the process-local window map. */ + wireModelId?: string; + /** Cursor request identity scope; normalized identically to request-builder continuity. */ + identityScope?: string; } = {}): CursorProtobufEventState { return { // Cursor provides no authoritative usage frame; token counts are heuristic estimates from @@ -261,6 +270,8 @@ export function createCursorProtobufEventState(options: { && options.estimatedInputTokens > 0 ? { estimatedInputTokens: options.estimatedInputTokens } : {}), + ...(options.wireModelId?.trim() ? { wireModelId: options.wireModelId.trim() } : {}), + identityScope: options.identityScope?.trim() || "local", }; } @@ -1255,11 +1266,19 @@ export function mapCursorProtobufServerMessage( if (state.terminated) return []; if (serverMessage.message.case === "conversationCheckpointUpdate") { - const usedTokens = serverMessage.message.value.tokenDetails?.usedTokens ?? 0; + const tokenDetails = serverMessage.message.value.tokenDetails; + const usedTokens = tokenDetails?.usedTokens ?? 0; // `usedTokens` is the ABSOLUTE conversation context size, not a per-turn output delta. Track it // separately (monotonic max) and surface it as `done.usage.totalTokens`; folding it into // `outputTokens` (which also accumulates `tokenDelta`) double-counts in Codex. See contextTokens. observeContextTokens(state, usedTokens); + // First checkpoints often send maxTokens=0 (senpi). Only a positive ceiling + // replaces the id heuristic for the next turn's overflow vs 429 size prior. + if (state.wireModelId) { + recordObservedCursorContextWindow(state.wireModelId, tokenDetails?.maxTokens, { + identityScope: state.identityScope, + }); + } return []; } diff --git a/src/adapters/cursor/types.ts b/src/adapters/cursor/types.ts index 9dc8702658..f3dd641a63 100644 --- a/src/adapters/cursor/types.ts +++ b/src/adapters/cursor/types.ts @@ -11,6 +11,11 @@ export interface CursorRequestedModelParameter { export interface CursorRunRequest { modelId: string; + /** + * Normalized Cursor identity scope carried from request parsing so checkpoint + * observations stay scope-local. Transport metadata only; never serialized on the wire. + */ + _cursorIdentityScope?: string; /** Cursor model-picker parameters encoded through AgentRunRequest.requested_model. */ requestedModelParameters?: readonly CursorRequestedModelParameter[]; /** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */ diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index c162191411..43e196d6a9 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -139,6 +139,23 @@ drops the whole textual buffer. A missing advertised-name set is fail-closed. Fi clears any held or suppressed prefix. Coverage lives in `tests/providers/cursor/cursor-protobuf-events.test.ts`. +## Observed checkpoint window + +`conversationCheckpointUpdate.tokenDetails.maxTokens` is the account-advertised +ceiling for that wire model. A positive value is stored in a process-local map +keyed by the normalized Cursor identity scope and model id +(`src/adapters/cursor/discovery.ts`) and preferred by `inferCursorContextWindow` +only for that scope. Missing scopes normalize to the distinct `local` scope, so +they cannot inherit an authenticated account's observation. The map evicts its +oldest insertion above 2,048 entries. Zero and missing values are ignored — the +first checkpoint is often 0. The next turn's +`cursorRequestSizeContext` feeds that window into the existing 0.5-window +overflow vs 429 prior so a tiny request against a plan-gated 32k ceiling stays +on the 429 class, while a request that is large relative to the real window +classifies as overflow. Coverage lives in +`tests/providers/cursor/cursor-errors.test.ts` and +`tests/providers/cursor/cursor-protobuf-events.test.ts`. + ## Overflow remint boundary `src/adapters/cursor.ts` surfaces the first bare context overflow before attempting conversation remint on later eligible requests. `cursorClientThreadOwner` recognizes both client thread aliases; `src/adapters/cursor/thread-continuity.ts` limits recovery to three remints per retained identity-scoped owner, with a one-hour idle TTL and 2,048-entry bound. Conversation-only requests have no stable owner and do not automatically remint. Quota/rate errors, tool-result resumes, partial output, local side effects, isolated helper/shadow requests and compaction remain fail-closed. Isolated requests neither consume the parent allowance nor invalidate its checkpoint. Eligible overflow checks refresh existing retention timestamps and LRU position even after the cap is exhausted, without allocating absent scopes. Retention expiry, eviction or process restart resets the in-memory allowance; this is not a persistent lifetime cap or semantic-progress policy. diff --git a/tests/providers/cursor/cursor-discovery.test.ts b/tests/providers/cursor/cursor-discovery.test.ts index 32656dd12d..200351c183 100644 --- a/tests/providers/cursor/cursor-discovery.test.ts +++ b/tests/providers/cursor/cursor-discovery.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { CURSOR_AUTO_WIRE_MODEL_ID, CURSOR_DEFAULT_CONTEXT_WINDOW, + CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES, CURSOR_ROUTER_MODEL_IDS, CURSOR_ROUTING_LEVELS, CURSOR_NO_VISION_MODELS, @@ -20,6 +21,8 @@ import { isCursorNativeWireModel, cursorNeedsExternalToolContinuation, normalizeCursorModels, + recordObservedCursorContextWindow, + resetObservedCursorContextWindowsForTests, } from "../../../src/adapters/cursor/discovery"; describe("Cursor discovery metadata", () => { @@ -231,6 +234,66 @@ describe("Cursor discovery metadata", () => { expect(cursorNeedsExternalToolContinuation("gpt-5.6-sol")).toBe(true); }); + describe("observed checkpoint maxTokens ceiling", () => { + afterEach(() => { + resetObservedCursorContextWindowsForTests(); + }); + + test("same-model observations are isolated by normalized identity scope", () => { + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, { identityScope: " account-a " }); + recordObservedCursorContextWindow("claude-4.6-sonnet", 64_000, { identityScope: "account-b" }); + + expect(inferCursorContextWindow("CLAUDE-4.6-SONNET", { identityScope: "account-a" })).toBe(32_000); + expect(inferCursorContextWindow("claude-4.6-sonnet", { identityScope: " account-b " })).toBe(64_000); + }); + + test("an unscoped lookup does not read a scoped observation", () => { + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, { identityScope: "account-a" }); + + expect(inferCursorContextWindow("claude-4.6-sonnet")).toBe(200_000); + }); + + test("zero, negative, missing, and non-finite maxTokens keep the heuristic", () => { + const options = { identityScope: "account-a" }; + recordObservedCursorContextWindow("claude-4.6-sonnet", 0, options); + recordObservedCursorContextWindow("claude-4.6-sonnet", undefined, options); + recordObservedCursorContextWindow("claude-4.6-sonnet", Number.NaN, options); + recordObservedCursorContextWindow("claude-4.6-sonnet", -8, options); + recordObservedCursorContextWindow("", 32_000, options); + expect(inferCursorContextWindow("claude-4.6-sonnet", options)).toBe(200_000); + }); + + test("an explicit observed argument outranks the process-local map", () => { + const identityScope = "account-a"; + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, { identityScope }); + expect(inferCursorContextWindow("claude-4.6-sonnet", { identityScope, observed: 8_000 })).toBe(8_000); + expect(inferCursorContextWindow("claude-4.6-sonnet", { identityScope, observed: 0 })).toBe(32_000); + }); + + test("reset clears observations from every identity scope", () => { + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, { identityScope: "account-a" }); + recordObservedCursorContextWindow("claude-4.6-sonnet", 64_000, { identityScope: "account-b" }); + + resetObservedCursorContextWindowsForTests(); + + expect(inferCursorContextWindow("claude-4.6-sonnet", { identityScope: "account-a" })).toBe(200_000); + expect(inferCursorContextWindow("claude-4.6-sonnet", { identityScope: "account-b" })).toBe(200_000); + }); + + test("evicts the oldest observation after the bounded capacity", () => { + recordObservedCursorContextWindow("oldest-model", 32_000, { identityScope: "account-oldest" }); + for (let index = 1; index <= CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES; index++) { + recordObservedCursorContextWindow(`model-${index}`, 32_000 + index, { identityScope: `account-${index}` }); + } + + expect(inferCursorContextWindow("oldest-model", { identityScope: "account-oldest" })) + .toBe(CURSOR_DEFAULT_CONTEXT_WINDOW); + expect(inferCursorContextWindow(`model-${CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES}`, { + identityScope: `account-${CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES}`, + })).toBe(32_000 + CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES); + }); + }); + test("normalizes Cursor checkpoint model affinity across prefix and effort", () => { expect(cursorCheckpointModelAffinityId("cursor/grok-4.6")).toBe( cursorCheckpointModelAffinityId("cursor-grok-4.6-low"), diff --git a/tests/providers/cursor/cursor-errors.test.ts b/tests/providers/cursor/cursor-errors.test.ts index 4d55bdd711..849659872f 100644 --- a/tests/providers/cursor/cursor-errors.test.ts +++ b/tests/providers/cursor/cursor-errors.test.ts @@ -1,10 +1,15 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { classifyCursorError, isCursorBenignCancelError, isCursorInvalidArgumentError, safeCursorErrorMessage, } from "../../../src/adapters/cursor/cursor-errors"; +import { + inferCursorContextWindow, + recordObservedCursorContextWindow, + resetObservedCursorContextWindowsForTests, +} from "../../../src/adapters/cursor/discovery"; import { inferHttpStatusFromAdapterMessage } from "../../../src/lib/errors"; describe("classifyCursorError", () => { @@ -200,4 +205,29 @@ describe("bare resource_exhausted size prior (devlog 260)", () => { expect(classifyCursorError("resource_exhausted: request body exceeds maximum allowed size", { estimatedInputTokens: 20, contextWindow: 200_000 })) .toBe("Cursor resource limit exceeded"); }); + + describe("observed checkpoint maxTokens feeds the size prior", () => { + afterEach(() => { + resetObservedCursorContextWindowsForTests(); + }); + + test("a 20-token request against an observed 32k ceiling stays 429", () => { + const options = { identityScope: "account-a" }; + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, options); + expect(inferCursorContextWindow("claude-4.6-sonnet", options)).toBe(32_000); + expect(classifyCursorError(BARE, { + estimatedInputTokens: 20, + contextWindow: inferCursorContextWindow("claude-4.6-sonnet", options), + })).toBe("Cursor rate limit exceeded"); + }); + + test("a request that is large relative to the observed ceiling stays overflow", () => { + const options = { identityScope: "account-a" }; + recordObservedCursorContextWindow("claude-4.6-sonnet", 32_000, options); + expect(classifyCursorError(BARE, { + estimatedInputTokens: 20_000, + contextWindow: inferCursorContextWindow("claude-4.6-sonnet", options), + })).toBe("Cursor context limit exceeded"); + }); + }); }); diff --git a/tests/providers/cursor/cursor-protobuf-events.test.ts b/tests/providers/cursor/cursor-protobuf-events.test.ts index 3cc461d09c..4dae165c06 100644 --- a/tests/providers/cursor/cursor-protobuf-events.test.ts +++ b/tests/providers/cursor/cursor-protobuf-events.test.ts @@ -1,5 +1,5 @@ import { create } from "@bufbuild/protobuf"; -import { describe, expect, spyOn, test } from "bun:test"; +import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { AgentServerMessageSchema, ConversationStateStructureSchema, @@ -21,6 +21,10 @@ import { mapCursorProtobufServerMessage, mapSyntheticMcpExecToToolEvents, } from "../../../src/adapters/cursor/protobuf-events"; +import { + inferCursorContextWindow, + resetObservedCursorContextWindowsForTests, +} from "../../../src/adapters/cursor/discovery"; import { MAX_PENDING_TEXT_TOOLCALL_BYTES } from "../../../src/adapters/cursor/text-toolcall"; import { resetDebugSettingsForTests } from "../../../src/lib/debug-settings"; import { createTranslatorBudget } from "../../../src/lib/translator-budget"; @@ -57,12 +61,15 @@ function mcpToolCall(toolName: string, args: Record) { }); } -function checkpointUpdate(usedTokens: number) { +function checkpointUpdate(usedTokens: number, maxTokens?: number) { return create(AgentServerMessageSchema, { message: { case: "conversationCheckpointUpdate", value: create(ConversationStateStructureSchema, { - tokenDetails: create(ConversationTokenDetailsSchema, { usedTokens }), + tokenDetails: create(ConversationTokenDetailsSchema, { + usedTokens, + ...(maxTokens !== undefined ? { maxTokens } : {}), + }), }), }, }); @@ -1397,6 +1404,32 @@ describe("#2472 the Cursor producer path for a silent empty turn", () => { }); +describe("observed checkpoint maxTokens ceiling", () => { + afterEach(() => { + resetObservedCursorContextWindowsForTests(); + }); + + test("a positive maxTokens records a process-local window for that wire model", () => { + const state = createCursorProtobufEventState({ wireModelId: "claude-4.6-sonnet" }); + expect(inferCursorContextWindow("claude-4.6-sonnet")).toBe(200_000); + expect(mapCursorProtobufServerMessage(checkpointUpdate(1_200, 32_000), state)).toEqual([]); + expect(inferCursorContextWindow("claude-4.6-sonnet")).toBe(32_000); + }); + + test("zero or missing maxTokens leaves the heuristic in place", () => { + const state = createCursorProtobufEventState({ wireModelId: "claude-4.6-sonnet" }); + expect(mapCursorProtobufServerMessage(checkpointUpdate(1_200, 0), state)).toEqual([]); + expect(mapCursorProtobufServerMessage(checkpointUpdate(1_200), state)).toEqual([]); + expect(inferCursorContextWindow("claude-4.6-sonnet")).toBe(200_000); + }); + + test("a checkpoint without wireModelId does not record a window", () => { + const state = createCursorProtobufEventState(); + expect(mapCursorProtobufServerMessage(checkpointUpdate(1_200, 32_000), state)).toEqual([]); + expect(inferCursorContextWindow("claude-4.6-sonnet")).toBe(200_000); + }); +}); + describe("#2472 end to end: the real producer output reaches the observer", () => { /** * The two halves were verified separately — the Cursor adapter can finalize a turn to a