From d16b1508613694a3a00e6edd68036520c94eccbf Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 28 Aug 2026 11:37:13 +0900 Subject: [PATCH] fix(cursor): bound external replay deduplication --- src/adapters/cursor/protobuf-request.ts | 20 ++++++++++++-------- tests/cursor-repetition-breaker.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/adapters/cursor/protobuf-request.ts b/src/adapters/cursor/protobuf-request.ts index 55dd1e9205..d7ff961d60 100644 --- a/src/adapters/cursor/protobuf-request.ts +++ b/src/adapters/cursor/protobuf-request.ts @@ -71,6 +71,8 @@ export const CURSOR_ROUTING_LEVEL_PARAMETER_ID = "optimization"; export const CURSOR_EXTERNAL_ROOT_BLOB_LIMIT = 192; /** Approximate prompt-size guard; tool schemas and protocol framing consume context separately. */ export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024; +/** Bound synchronous replay construction before the smaller wire-size limits are applied. */ +export const CURSOR_EXTERNAL_REPLAY_MESSAGE_LIMIT = 4096; /** * Action text for external-model tool-result continuations. Native models keep @@ -224,7 +226,7 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR // Collapse consecutive duplicates into one entry + a count marker, and count collapses so a // strategy-change note can be appended when the pattern is severe. let lastReplayText: string | undefined; - let lastReplayEntry: RootBlobCandidate | undefined; + let lastReplayEntryIndex: number | undefined; let collapsedRepeats = 0; let maxRunLength = 1; let currentRun = 1; @@ -234,7 +236,7 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR opts: { messageIndex: number; text?: string }, normalized: string, ): void => { - if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) { + if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntryIndex !== undefined) { collapsedRepeats++; currentRun++; if (currentRun > maxRunLength) maxRunLength = currentRun; @@ -244,19 +246,21 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR role, opts, ); - entries[entries.indexOf(lastReplayEntry)] = replacement; - lastReplayEntry = replacement; + entries[lastReplayEntryIndex] = replacement; return; } currentRun = 1; const entry = rootBlobCandidate(payload, role, opts); entries.push(entry); lastReplayText = normalized; - lastReplayEntry = entry; + lastReplayEntryIndex = entries.length - 1; }; - for (let i = 0; i < messages.length; i++) { - if (i === activeUserIndex) break; + const replayEnd = activeUserIndex < 0 ? messages.length : activeUserIndex; + const replayStart = externalModel + ? Math.max(0, replayEnd - CURSOR_EXTERNAL_REPLAY_MESSAGE_LIMIT) + : 0; + for (let i = replayStart; i < replayEnd; i++) { const message = messages[i]; if (!message) continue; if (message.role === "user" || message.role === "developer") { @@ -266,7 +270,7 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR // before tokenization (`usedTokens: 0`, then invalid_argument). if (text.length > 0) { lastReplayText = undefined; - lastReplayEntry = undefined; + lastReplayEntryIndex = undefined; currentRun = 1; entries.push(rootBlobCandidate({ role: "user", diff --git a/tests/cursor-repetition-breaker.test.ts b/tests/cursor-repetition-breaker.test.ts index 63708f7a32..82f60af68c 100644 --- a/tests/cursor-repetition-breaker.test.ts +++ b/tests/cursor-repetition-breaker.test.ts @@ -1,6 +1,9 @@ import { describe, expect, test } from "bun:test"; import { fromBinary } from "@bufbuild/protobuf"; -import { encodeCursorRunRequest } from "../src/adapters/cursor/protobuf-request"; +import { + CURSOR_EXTERNAL_REPLAY_MESSAGE_LIMIT, + encodeCursorRunRequest, +} from "../src/adapters/cursor/protobuf-request"; import { handleCursorNativeKv } from "../src/adapters/cursor/native-exec"; import { create } from "@bufbuild/protobuf"; import { @@ -95,4 +98,24 @@ describe("cursor external-replay repetition breaker (devlog 260826 gap-9)", () = const texts = rootTexts(encode(messages)); expect(texts.filter(text => text === REPEAT)).toHaveLength(2); }); + + test("bounds replay construction before processing an oversized history", () => { + const messages: OcxMessage[] = [ + { role: "user", content: "old turn", timestamp: 1 }, + ...Array.from({ length: 5 }, (_, index) => ({ + role: "assistant" as const, + content: REPEAT, + timestamp: index + 2, + })), + ...Array.from({ length: CURSOR_EXTERNAL_REPLAY_MESSAGE_LIMIT }, (_, index) => ({ + role: "assistant" as const, + content: index % 2 === 0 ? "recent A" : "recent B", + timestamp: index + 7, + })), + { role: "user", content: "continue", timestamp: CURSOR_EXTERNAL_REPLAY_MESSAGE_LIMIT + 7 }, + ] as OcxMessage[]; + + const texts = rootTexts(encode(messages)); + expect(texts.some(text => text.includes("Take a DIFFERENT action now"))).toBe(false); + }); });