From 66c187fd7648b3daf2b042ddea1a0bf10f254c16 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 03:58:04 +0000 Subject: [PATCH] fix(agents): preserve delivered assistant replies in session repair --- src/agents/session-file-repair.test.ts | 7 ++++--- src/agents/session-file-repair.ts | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/agents/session-file-repair.test.ts b/src/agents/session-file-repair.test.ts index b8cbfc545ec..f0151e96843 100644 --- a/src/agents/session-file-repair.test.ts +++ b/src/agents/session-file-repair.test.ts @@ -117,7 +117,7 @@ describe("repairSessionFileIfNeeded", () => { errorMessage: "transient stream failure", }, }; - // Follow-up so the session doesn't end on assistant (trailing-trim is tested separately). + // Follow-up keeps this case focused on empty error-turn repair. const followUp = { type: "message", id: "msg-3", @@ -172,6 +172,7 @@ describe("repairSessionFileIfNeeded", () => { expect(result.repaired).toBe(true); expect(result.rewrittenUserMessages).toBe(1); + expect(result.droppedBlankUserMessages).toBe(0); expect(debug.mock.calls[0]?.[0]).toContain("rewrote 1 user message(s)"); const repaired = await fs.readFile(file, "utf-8"); @@ -292,7 +293,7 @@ describe("repairSessionFileIfNeeded", () => { stopReason: "stop", }, }; - // Follow-up so the session doesn't end on assistant (trailing-trim is tested separately). + // Follow-up keeps this case focused on silent-reply preservation. const followUp = { type: "message", id: "msg-3", @@ -507,7 +508,7 @@ describe("repairSessionFileIfNeeded", () => { stopReason: "error", }, }; - // Follow-up so the session doesn't end on assistant (trailing-trim is tested separately). + // Follow-up keeps this case focused on idempotent empty error-turn repair. const followUp = { type: "message", id: "msg-3", diff --git a/src/agents/session-file-repair.ts b/src/agents/session-file-repair.ts index ed72b13c9c1..106ab06fb64 100644 --- a/src/agents/session-file-repair.ts +++ b/src/agents/session-file-repair.ts @@ -10,6 +10,7 @@ type RepairReport = { repaired: boolean; droppedLines: number; rewrittenAssistantMessages?: number; + droppedBlankUserMessages?: number; rewrittenUserMessages?: number; backupPath?: string; reason?: string; @@ -66,7 +67,10 @@ function rewriteAssistantEntryWithEmptyContent(entry: SessionMessageEntry): Sess }; } -type UserEntryRepair = { kind: "rewrite"; entry: SessionMessageEntry } | { kind: "keep" }; +type UserEntryRepair = + | { kind: "drop" } + | { kind: "rewrite"; entry: SessionMessageEntry } + | { kind: "keep" }; function repairUserEntryWithBlankTextContent(entry: SessionMessageEntry): UserEntryRepair { const content = entry.message.content; @@ -134,6 +138,7 @@ function repairUserEntryWithBlankTextContent(entry: SessionMessageEntry): UserEn function buildRepairSummaryParts(params: { droppedLines: number; rewrittenAssistantMessages: number; + droppedBlankUserMessages: number; rewrittenUserMessages: number; }): string { const parts: string[] = []; @@ -143,6 +148,9 @@ function buildRepairSummaryParts(params: { if (params.rewrittenAssistantMessages > 0) { parts.push(`rewrote ${params.rewrittenAssistantMessages} assistant message(s)`); } + if (params.droppedBlankUserMessages > 0) { + parts.push(`dropped ${params.droppedBlankUserMessages} blank user message(s)`); + } if (params.rewrittenUserMessages > 0) { parts.push(`rewrote ${params.rewrittenUserMessages} user message(s)`); } @@ -176,6 +184,7 @@ export async function repairSessionFileIfNeeded(params: { const entries: unknown[] = []; let droppedLines = 0; let rewrittenAssistantMessages = 0; + let droppedBlankUserMessages = 0; let rewrittenUserMessages = 0; for (const line of lines) { @@ -197,6 +206,10 @@ export async function repairSessionFileIfNeeded(params: { ((entry as { message: { role?: unknown } }).message?.role ?? undefined) === "user" ) { const repairedUser = repairUserEntryWithBlankTextContent(entry as SessionMessageEntry); + if (repairedUser.kind === "drop") { + droppedBlankUserMessages += 1; + continue; + } if (repairedUser.kind === "rewrite") { entries.push(repairedUser.entry); rewrittenUserMessages += 1; @@ -220,7 +233,12 @@ export async function repairSessionFileIfNeeded(params: { return { repaired: false, droppedLines, reason: "invalid session header" }; } - if (droppedLines === 0 && rewrittenAssistantMessages === 0 && rewrittenUserMessages === 0) { + if ( + droppedLines === 0 && + rewrittenAssistantMessages === 0 && + droppedBlankUserMessages === 0 && + rewrittenUserMessages === 0 + ) { return { repaired: false, droppedLines: 0 }; } @@ -252,6 +270,7 @@ export async function repairSessionFileIfNeeded(params: { repaired: false, droppedLines, rewrittenAssistantMessages, + droppedBlankUserMessages, rewrittenUserMessages, reason: `repair failed: ${err instanceof Error ? err.message : "unknown error"}`, }; @@ -261,6 +280,7 @@ export async function repairSessionFileIfNeeded(params: { `session file repaired: ${buildRepairSummaryParts({ droppedLines, rewrittenAssistantMessages, + droppedBlankUserMessages, rewrittenUserMessages, })} (${path.basename(sessionFile)})`, ); @@ -268,6 +288,7 @@ export async function repairSessionFileIfNeeded(params: { repaired: true, droppedLines, rewrittenAssistantMessages, + droppedBlankUserMessages, rewrittenUserMessages, backupPath, };