fix: resolve race condition in escape interrupt handling (#349)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-22 15:46:37 -08:00
committed by GitHub
parent 73a964f111
commit 5dac7172e6

View File

@@ -862,8 +862,12 @@ export default function App({
clearCompletedSubagents();
while (true) {
// Capture the signal BEFORE any async operations
// This prevents a race where handleInterrupt nulls the ref during await
const signal = abortControllerRef.current?.signal;
// Check if cancelled before starting new stream
if (abortControllerRef.current?.signal.aborted) {
if (signal?.aborted) {
setStreaming(false);
return;
}
@@ -874,6 +878,12 @@ export default function App({
currentInput,
);
// Check again after network call - user may have pressed Escape during sendMessageStream
if (signal?.aborted) {
setStreaming(false);
return;
}
// Define callback to sync agent state on first message chunk
// This ensures the UI shows the correct model as early as possible
const syncAgentState = async () => {
@@ -929,7 +939,7 @@ export default function App({
stream,
buffersRef.current,
refreshDerivedThrottled,
abortControllerRef.current?.signal,
signal, // Use captured signal, not ref (which may be nulled by handleInterrupt)
syncAgentState,
);
@@ -1510,6 +1520,9 @@ export default function App({
// If EAGER_CANCEL is enabled, immediately stop everything client-side first
if (EAGER_CANCEL) {
// Prevent multiple handleInterrupt calls while state updates are pending
setInterruptRequested(true);
// Abort the stream via abort signal
if (abortControllerRef.current) {
abortControllerRef.current.abort();
@@ -1540,12 +1553,13 @@ export default function App({
// Silently ignore - cancellation already happened client-side
});
// Reset cancellation flag after cleanup is complete.
// Reset cancellation flags after cleanup is complete.
// This allows the dequeue effect to process any queued messages.
// We use setTimeout to ensure React state updates (setStreaming, etc.)
// have been processed before the dequeue effect runs.
setTimeout(() => {
userCancelledRef.current = false;
setInterruptRequested(false);
}, 0);
return;