fix(cursor): degrade K3 same-model history missing thinking

assertCursorKimiK3HistoryReplayable threw for any assistant turn lacking a
non-empty thinking block, including same-model kimi-k3 turns whose stream
carried no thinkingDelta events. Such a turn is persisted with only
text/toolCall blocks, so every subsequent turn failed locally before any
request, permanently bricking the session.

Split the two failure modes: foreign history still hard-errors (another
model's turns cannot replay K3 reasoning), but a same-model turn missing
thinking now degrades to a one-time warning and replays without the
reasoning part via buildCursorAssistantContent, which already omits it.

Fixes #7516
This commit is contained in:
roboomp
2026-08-03 15:19:17 +00:00
parent 01c1f91ff5
commit 467437a47e
3 changed files with 65 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Cursor `kimi-k3` sessions bricking permanently when a same-model assistant turn was persisted without thinking blocks (a turn whose stream carried no `thinkingDelta` events). `assertCursorKimiK3HistoryReplayable` now only hard-errors on genuinely foreign history; same-model turns missing thinking degrade to a one-time warning and replay without the reasoning part ([#7516](https://github.com/can1357/oh-my-pi/issues/7516)).
## [17.2.6] - 2026-08-03
### Added
+22 -3
View File
@@ -144,6 +144,7 @@ import { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
import {
$env,
logger,
parseJsonWithRepair,
parseStreamingJson,
parseStreamingJsonThrottled,
@@ -4113,16 +4114,34 @@ function assertCursorKimiK3HistoryReplayable(
): void {
if (!targetModelId || !isKimiK3ModelId(targetModelId)) return;
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
let sawSameModelMissingThinking = false;
for (let i = 0; i < historyEnd; i++) {
const msg = messages[i];
if (msg.role !== "assistant") continue;
const isSameCursorModel = msg.api === "cursor-agent" && msg.provider === "cursor" && msg.model === targetModelId;
const hasThinking = msg.content.some(item => item.type === "thinking" && item.thinking.length > 0);
if (!isSameCursorModel || !hasThinking) {
if (!isSameCursorModel) {
// Foreign history genuinely cannot replay K3 thinking: another model's
// turns carry no K3-signed reasoning to reconstruct, so continuation
// is unsafe and must fail hard.
throw new AIError.ValidationError(
`Cursor ${targetModelId} requires complete same-model thinking history; start a new session instead of continuing history from ${msg.provider}/${msg.model}.`,
`Cursor ${targetModelId} cannot continue history from a different model (${msg.provider}/${msg.model}); start a new session.`,
);
}
const hasThinking = msg.content.some(item => item.type === "thinking" && item.thinking.length > 0);
if (!hasThinking) sawSameModelMissingThinking = true;
}
if (sawSameModelMissingThinking) {
// A same-model turn whose stream carried no `thinkingDelta` events is
// persisted without thinking blocks. `buildCursorAssistantContent` still
// replays its text/tool-call structure and simply omits the reasoning
// part, so the history remains usable — degrade with a one-time warning
// rather than permanently bricking the session. Per Kimi's
// preserved-thinking caveat, K3 generation may be less stable across the
// affected span.
logger.warn(
"Cursor kimi-k3 history contains a same-model assistant turn without thinking blocks; replaying without reasoning for that span (generation may be less stable)",
{ model: targetModelId },
);
}
}
+39 -1
View File
@@ -757,10 +757,48 @@ describe("Cursor history encoding", () => {
];
expect(() => buildCursorHistoryForTest(messages, undefined, "kimi-k3-high")).toThrow(
"start a new session instead of continuing history from anthropic/claude-4.6-opus-high",
"cannot continue history from a different model (anthropic/claude-4.6-opus-high)",
);
});
it("replays a same-model K3 turn missing thinking instead of bricking the session", () => {
const messages: Context["messages"] = [
{ role: "user", content: "Do a big multi-tool task", timestamp: 1 },
cursorAssistant(
"kimi-k3-high",
[{ type: "toolCall", id: "call-read", name: "read", arguments: { path: "package.json" } }],
2,
"toolUse",
),
{
role: "toolResult",
toolCallId: "call-read",
toolName: "read",
content: [{ type: "text", text: "package contents" }],
isError: false,
timestamp: 3,
},
cursorAssistant("kimi-k3-high", [{ type: "text", text: "Done." }], 4),
{ role: "user", content: "Continue the same session", timestamp: 5 },
];
const history = buildCursorHistoryForTest(messages, undefined, "kimi-k3-high");
expect(history.rootPromptMessagesJson).toEqual([
{ role: "user", content: [{ type: "text", text: "Do a big multi-tool task" }] },
{
role: "assistant",
content: [{ type: "tool-call", toolCallId: "call-read", toolName: "read", args: { path: "package.json" } }],
},
{
role: "tool",
id: "call-read",
content: [{ type: "tool-result", toolName: "read", toolCallId: "call-read", result: "package contents" }],
},
{ role: "assistant", content: [{ type: "text", text: "Done." }] },
]);
});
it("keeps non-K3 Cursor thinking out of model-facing history", () => {
const messages: Context["messages"] = [
{ role: "user", content: "Inspect package.json", timestamp: 1 },