feat(coding-agent/session): preserved signed thinking blocks during demotion
- Updated the demotion logic to treat complete, signed thinking blocks as stable content that terminates an interrupted stream. - Protected signed thinking runs from being stripped when processing interrupted agent messages.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed interrupted reasoning blocks being incorrectly stripped when they contained a valid signature
|
||||
- Fixed interrupted thinking being lost in LLM provider requests after user interrupts by properly stripping trailing reasoning blocks from assistant turns while preserving them in the UI and session history.
|
||||
- Fixed the live todo HUD going stale during long tool-use loops by introducing a mid-run reconciliation reminder that prompts the agent to update incomplete items.
|
||||
- Fixed resumed OpenAI and OpenAI-Codex sessions losing encrypted reasoning and native assistant turns during rehydration.
|
||||
|
||||
@@ -105,4 +105,21 @@ describe("convertToLlm", () => {
|
||||
"thinking",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a signed (complete) trailing thinking block in the assistant LLM view even with a continuity message", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
abortedAssistant([
|
||||
{ type: "text", text: "partial answer" },
|
||||
{ type: "thinking", thinking: "complete reasoning", thinkingSignature: "sig" },
|
||||
]),
|
||||
interruptedThinkingContinuity(),
|
||||
];
|
||||
|
||||
const llm = convertToLlm(messages);
|
||||
const assistant = llm.find(entry => entry.role === "assistant");
|
||||
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual([
|
||||
"text",
|
||||
"thinking",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,12 +56,14 @@ export interface DemotedInterruptedThinking {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a trailing interrupted-thinking run from an assistant message.
|
||||
* Demote a trailing run of *incomplete* interrupted-thinking from an assistant
|
||||
* message — reasoning that was still streaming when the user aborted.
|
||||
*
|
||||
* Only visible, non-empty `thinking` blocks at the meaningful tail are demoted.
|
||||
* Trailing empty text placeholders are ignored and omitted from the stripped
|
||||
* content. Text, tool-call, redacted/encrypted-only, and empty-thinking tails
|
||||
* are left unchanged.
|
||||
* A block joins the run only when it is a non-empty `thinking` block with no
|
||||
* `thinkingSignature`. A signed/complete thinking block (Anthropic signature,
|
||||
* OpenAI reasoning item id) is safely replayable, so it ends the run and stays
|
||||
* in place — as do `redactedThinking` encrypted blobs, text, tool calls,
|
||||
* empty-thinking blocks, and trailing empty text placeholders.
|
||||
*/
|
||||
export function demoteInterruptedThinking(
|
||||
message: Pick<AssistantMessage, "content">,
|
||||
@@ -79,7 +81,7 @@ export function demoteInterruptedThinking(
|
||||
let runStart = scanEnd;
|
||||
while (runStart > 0) {
|
||||
const block = content[runStart - 1]!;
|
||||
if (block.type !== "thinking" || block.thinking.trim().length === 0) {
|
||||
if (block.type !== "thinking" || block.thinking.trim().length === 0 || block.thinkingSignature) {
|
||||
break;
|
||||
}
|
||||
runStart--;
|
||||
|
||||
@@ -69,4 +69,26 @@ describe("demoteInterruptedThinking", () => {
|
||||
expect(demoteContent([{ type: "thinking", thinking: " \n\t" }])).toBeUndefined();
|
||||
expect(demoteContent([{ type: "redactedThinking", data: "encrypted" }])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves a non-empty signed thinking tail as native replayable reasoning", () => {
|
||||
expect(
|
||||
demoteContent([
|
||||
{ type: "text", text: "Visible answer." },
|
||||
{ type: "thinking", thinking: "Complete signed reasoning", thinkingSignature: "sig" },
|
||||
]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("demotes only the unsigned tail and keeps an earlier signed thinking block", () => {
|
||||
expect(
|
||||
demoteContent([
|
||||
{ type: "thinking", thinking: "Complete signed reasoning", thinkingSignature: "sig" },
|
||||
{ type: "thinking", thinking: "Interrupted unsigned tail" },
|
||||
]),
|
||||
).toEqual({
|
||||
reasoning: "Interrupted unsigned tail",
|
||||
strippedContent: [{ type: "thinking", thinking: "Complete signed reasoning", thinkingSignature: "sig" }],
|
||||
blockCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user