Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/adapters/cursor/protobuf-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Comment on lines +260 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align the capped replay window to a complete turn

When more than 4,096 messages follow the most recent historical user message, this cutoff starts on an assistant/tool-result entry. The later complete-turn selector requires a user root, so with the history constructed by the new test (old turn, 4,101 assistant messages, then the active user), it rejects the orphaned suffix and sends only the system root; the external model receives the active “continue” action with none of the conversation being continued. Anchor the bounded suffix at a user/developer boundary, or explicitly retain the preceding user message, so limiting construction cannot erase all replay context.

Useful? React with 👍 / 👎.

for (let i = replayStart; i < replayEnd; i++) {
const message = messages[i];
if (!message) continue;
if (message.role === "user" || message.role === "developer") {
Expand All @@ -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",
Expand Down
25 changes: 24 additions & 1 deletion tests/cursor-repetition-breaker.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
});
});
Loading