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:
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user