fix(cursor): gated turn thinking steps to same-model K3

- Applied the same same-model K3 replay guard to ConversationState.turns thinking steps so foreign or non-K3 reasoning never leaks into Cursor turn history.

Fixes #7184
This commit is contained in:
roboomp
2026-07-31 15:48:08 +00:00
parent 722b154266
commit b35adb2171
2 changed files with 46 additions and 7 deletions
+12 -3
View File
@@ -4311,6 +4311,7 @@ function buildConversationTurns(
messages: Message[],
blobStore: Map<string, Uint8Array>,
activeUserMessageIndex = findLastUserMessageIndex(messages),
targetModelId?: string,
): Uint8Array[] {
const turns: Uint8Array[] = [];
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
@@ -4365,7 +4366,10 @@ function buildConversationTurns(
},
});
} else if (item.type === "thinking") {
if (!item.thinking) continue;
// Same guard as root-prompt replay: only same-model Cursor K3
// thinking is replayed, so foreign/hidden reasoning never leaks
// into Cursor's turn history as native thinking.
if (!item.thinking || !canReplayCursorThinking(stepMsg, targetModelId)) continue;
step = create(ConversationStepSchema, {
message: {
case: "thinkingMessage",
@@ -4431,7 +4435,7 @@ export function buildCursorHistoryForTest(
).map(blobId => JSON.parse(new TextDecoder().decode(readCursorBlob(blobStore, blobId))));
const turnUserMessagesJson: JsonValue[] = [];
const turnStepMessagesJson: JsonValue[][] = [];
for (const turnBlobId of buildConversationTurns(messages, blobStore, activeUserMessageIndex)) {
for (const turnBlobId of buildConversationTurns(messages, blobStore, activeUserMessageIndex, targetModelId)) {
const turn = fromBinary(ConversationTurnStructureSchema, readCursorBlob(blobStore, turnBlobId));
if (turn.turn.case !== "agentConversationTurn") {
continue;
@@ -4535,7 +4539,12 @@ function buildGrpcRequest(
// Build conversation turns from prior messages, excluding only the active user message
// when the request is sending one. Resume actions must preserve trailing tool results.
const turns = buildConversationTurns(context.messages, blobStore, activeUserMessage ? activeUserMessageIndex : -1);
const turns = buildConversationTurns(
context.messages,
blobStore,
activeUserMessage ? activeUserMessageIndex : -1,
model.id,
);
// Build `rootPromptMessagesJson` from prior messages. Cursor's server uses this
// field (not `turns[]`) to construct the actual model prompt; if we only send the
+34 -4
View File
@@ -747,13 +747,43 @@ describe("Cursor history encoding", () => {
{ role: "assistant", content: [{ type: "text", text: "Visible answer." }] },
]);
expect(history.turnStepMessagesJson).toEqual([
[
expect.objectContaining({ thinkingMessage: { text: "Internal reasoning." } }),
expect.objectContaining({ assistantMessage: { text: "Visible answer." } }),
],
[expect.objectContaining({ assistantMessage: { text: "Visible answer." } })],
]);
});
it("keeps foreign thinking out of turns when switching to a non-K3 Cursor model", () => {
const messages: Context["messages"] = [
{ role: "user", content: "Plan this change.", timestamp: 1 },
{
...cursorAssistant(
"claude-4.6-opus-high",
[
{ type: "thinking", thinking: "Foreign signed reasoning.", thinkingSignature: "signature" },
{ type: "text", text: "Here is the plan." },
],
2,
),
api: "anthropic-messages",
provider: "anthropic",
},
{ role: "user", content: "Continue.", timestamp: 3 },
];
const history = buildCursorHistoryForTest(messages, undefined, "cursor-composer-2.5");
expect(history.rootPromptMessagesJson).toEqual([
{ role: "user", content: [{ type: "text", text: "Plan this change." }] },
{ role: "assistant", content: [{ type: "text", text: "Here is the plan." }] },
]);
expect(history.turnStepMessagesJson).toEqual([
[expect.objectContaining({ assistantMessage: { text: "Here is the plan." } })],
]);
for (const steps of history.turnStepMessagesJson) {
for (const step of steps) {
expect(step).not.toHaveProperty("thinkingMessage");
}
}
});
it("preserves image-only user turns in root prompt history and conversation turns", () => {
const imageData = "aW1hZ2U=";
const history = buildCursorHistoryForTest([