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:
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user