From b35adb2171f433bca28ea8f8dca4fb89a355493a Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 31 Jul 2026 15:48:08 +0000 Subject: [PATCH] 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 --- packages/ai/src/providers/cursor.ts | 15 ++++++-- packages/ai/test/cursor-exec-handlers.test.ts | 38 +++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index 62f1f7ba3..79958f2f3 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -4311,6 +4311,7 @@ function buildConversationTurns( messages: Message[], blobStore: Map, 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 diff --git a/packages/ai/test/cursor-exec-handlers.test.ts b/packages/ai/test/cursor-exec-handlers.test.ts index ef3ab7de0..619e7d05b 100644 --- a/packages/ai/test/cursor-exec-handlers.test.ts +++ b/packages/ai/test/cursor-exec-handlers.test.ts @@ -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([