From 1dc703e532f5a9510cc989059a119d8cb325955b Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 29 Dec 2025 13:57:41 -0800 Subject: [PATCH] fix: prevent post-cancel chunks from rendering after ESC interrupt (#416) Co-authored-by: Letta --- src/cli/App.tsx | 9 +++++++-- src/cli/helpers/accumulator.ts | 7 +++++++ src/cli/helpers/stream.ts | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 221ebed..9e79750 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1095,8 +1095,13 @@ export default function App({ sessionStatsRef.current.endTurn(apiDurationMs); sessionStatsRef.current.updateUsageFromBuffers(buffersRef.current); - // Immediate refresh after stream completes to show final state - refreshDerived(); + const wasInterrupted = !!buffersRef.current.interrupted; + + // Immediate refresh after stream completes to show final state unless + // the user already cancelled (handleInterrupt rendered the UI). + if (!wasInterrupted) { + refreshDerived(); + } // Case 1: Turn ended normally if (stopReason === "end_turn") { diff --git a/src/cli/helpers/accumulator.ts b/src/cli/helpers/accumulator.ts index 72ea7f3..83a904d 100644 --- a/src/cli/helpers/accumulator.ts +++ b/src/cli/helpers/accumulator.ts @@ -219,6 +219,13 @@ function extractTextPart(v: unknown): string { // Feed one SDK chunk; mutate buffers in place. export function onChunk(b: Buffers, chunk: LettaStreamingResponse) { + // Skip processing if stream was interrupted mid-turn. handleInterrupt already + // rendered the cancellation state, so we should ignore any buffered chunks + // that arrive before drainStream exits. + if (b.interrupted) { + return; + } + // TODO remove once SDK v1 has proper typing for in-stream errors // Check for streaming error objects (not typed in SDK but emitted by backend) // Note: Error handling moved to catch blocks in App.tsx and headless.ts diff --git a/src/cli/helpers/stream.ts b/src/cli/helpers/stream.ts index 659c3b7..e6a31c3 100644 --- a/src/cli/helpers/stream.ts +++ b/src/cli/helpers/stream.ts @@ -71,6 +71,9 @@ export async function drainStream( } else if (abortSignal?.aborted) { // Already aborted before we started abortedViaListener = true; + if (stream.controller && !stream.controller.signal.aborted) { + stream.controller.abort(); + } } try {