From c868282453560376a026f2da7d5524417b44ce11 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 22 Dec 2025 21:35:09 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20use=20depth=20counter=20for=20processCon?= =?UTF-8?q?versation=20guard=20instead=20of=20strea=E2=80=A6=20(#359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Letta --- src/cli/App.tsx | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 8e7dbf1..29daff8 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -353,6 +353,11 @@ export default function App({ // Uses synced state to keep ref in sync for reliable async checks const [streaming, setStreaming, streamingRef] = useSyncedState(false); + // Guard ref for preventing concurrent processConversation calls + // Separate from streaming state which may be set early for UI responsiveness + // Tracks depth to allow intentional reentry while blocking parallel calls + const processingConversationRef = useRef(0); + // Whether an interrupt has been requested for the current stream const [interruptRequested, setInterruptRequested] = useState(false); @@ -840,8 +845,18 @@ export default function App({ const processConversation = useCallback( async ( initialInput: Array, + options?: { allowReentry?: boolean }, ): Promise => { const currentInput = initialInput; + const allowReentry = options?.allowReentry ?? false; + + // Guard against concurrent processConversation calls + // This can happen if user submits two messages in quick succession + // Uses dedicated ref (not streamingRef) since streaming may be set early for UI responsiveness + if (processingConversationRef.current > 0 && !allowReentry) { + return; + } + processingConversationRef.current += 1; try { // Check if user hit escape before we started @@ -850,12 +865,6 @@ export default function App({ return; } - // Guard against concurrent processConversation calls - // This can happen if user submits two messages in quick succession - if (streamingRef.current) { - return; - } - setStreaming(true); abortControllerRef.current = new AbortController(); @@ -1286,12 +1295,15 @@ export default function App({ setThinkingMessage(getRandomThinkingVerb()); refreshDerived(); - await processConversation([ - { - type: "approval", - approvals: allResults, - }, - ]); + await processConversation( + [ + { + type: "approval", + approvals: allResults, + }, + ], + { allowReentry: true }, + ); return; } @@ -1473,6 +1485,10 @@ export default function App({ refreshDerived(); } finally { abortControllerRef.current = null; + processingConversationRef.current = Math.max( + 0, + processingConversationRef.current - 1, + ); } }, [ @@ -3258,7 +3274,7 @@ DO NOT respond to these messages or otherwise consider them in your response unl buffersRef.current.interrupted = false; // Rotate to a new thinking message for this turn setThinkingMessage(getRandomThinkingVerb()); - // Show streaming state immediately for responsiveness + // Show streaming state immediately for responsiveness (pending approval check takes ~100ms) setStreaming(true); refreshDerived();