From 81ac1670abbf0a41753ef87397e28a7932d4e207 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:44:46 -0800 Subject: [PATCH] fix: resolve ghost assistant messages when Anthropic returns [text, thinking, text] (#956) Co-authored-by: Letta --- src/cli/helpers/accumulator.ts | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/cli/helpers/accumulator.ts b/src/cli/helpers/accumulator.ts index ef13cfb..f005253 100644 --- a/src/cli/helpers/accumulator.ts +++ b/src/cli/helpers/accumulator.ts @@ -481,6 +481,28 @@ function resolveAssistantLineId( canonicalFromMessageId || canonicalFromOtid || messageId || otid; if (!canonical) return undefined; + // When a new otid arrives whose messageId maps to an already-finished line, + // start a fresh canonical so the new content block gets its own line. + // This handles Anthropic responses like [text, thinking, text] where both + // text blocks share the same message id but need separate rendering lifecycles + // (the first gets committed to static before the second starts streaming). + if (otid && !canonicalFromOtid && canonicalFromMessageId) { + const existingLineId = resolveLineIdForKind( + b, + canonicalFromMessageId, + "assistant", + ); + const existingLine = b.byId.get(existingLineId); + if ( + existingLine && + existingLine.kind === "assistant" && + "phase" in existingLine && + existingLine.phase === "finished" + ) { + canonical = otid; + } + } + // If both aliases exist but disagree, prefer the one that already has a line. if ( canonicalFromMessageId && @@ -538,6 +560,25 @@ function resolveReasoningLineId( canonicalFromMessageId || canonicalFromOtid || messageId || otid; if (!canonical) return undefined; + // Same fix as resolveAssistantLineId: when a new otid maps to a + // finished reasoning line via messageId, start a fresh canonical. + if (otid && !canonicalFromOtid && canonicalFromMessageId) { + const existingLineId = resolveLineIdForKind( + b, + canonicalFromMessageId, + "reasoning", + ); + const existingLine = b.byId.get(existingLineId); + if ( + existingLine && + existingLine.kind === "reasoning" && + "phase" in existingLine && + existingLine.phase === "finished" + ) { + canonical = otid; + } + } + if ( canonicalFromMessageId && canonicalFromOtid &&