fix(queue): dequeueInFlightRef lock to prevent duplicate dequeue submissions (#1479)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
cthomas
2026-03-23 22:53:49 -07:00
committed by GitHub
parent af28a3f744
commit b4d133a32f
2 changed files with 16 additions and 3 deletions

View File

@@ -1899,6 +1899,8 @@ export default function App({
// Epoch counter to force dequeue effect re-run when refs change but state doesn't
// Incremented when userCancelledRef is reset while messages are queued
const [dequeueEpoch, setDequeueEpoch] = useState(0);
// Strict lock to ensure dequeue submit path is at-most-once while onSubmit is in flight.
const dequeueInFlightRef = useRef(false);
// Track last dequeued message for restoration on error
// If an error occurs after dequeue, we restore this to the input field (if input is empty)
@@ -10695,7 +10697,8 @@ ${SYSTEM_REMINDER_CLOSE}
!anySelectorOpen && // Don't dequeue while a selector/overlay is open
!waitingForQueueCancelRef.current && // Don't dequeue while waiting for cancel
!userCancelledRef.current && // Don't dequeue if user just cancelled
!abortControllerRef.current // Don't dequeue while processConversation is still active
!abortControllerRef.current && // Don't dequeue while processConversation is still active
!dequeueInFlightRef.current // Don't dequeue while previous dequeue submit is still in flight
) {
// consumeItems(n) fires onDequeued → setQueueDisplay(prev => prev.slice(n)).
const batch = tuiQueueRef.current?.consumeItems(queueLen);
@@ -10725,7 +10728,16 @@ ${SYSTEM_REMINDER_CLOSE}
// Submit via normal flow — overrideContentPartsRef carries rich content parts.
overrideContentPartsRef.current = queuedContentParts;
onSubmitRef.current(concatenatedMessage);
// Lock prevents re-entrant dequeue if deps churn before processConversation
// sets abortControllerRef (which is the normal long-term gate).
dequeueInFlightRef.current = true;
void onSubmitRef.current(concatenatedMessage).finally(() => {
dequeueInFlightRef.current = false;
// If more items arrived while in-flight, bump epoch so the effect re-runs.
if ((tuiQueueRef.current?.length ?? 0) > 0) {
setDequeueEpoch((e) => e + 1);
}
});
} else if (hasAnythingQueued) {
// Log why dequeue was blocked (useful for debugging stuck queues)
debugLog(

View File

@@ -33,7 +33,8 @@ describe("queue ordering wiring", () => {
// Queue is now drained via QueueRuntime.consumeItems; setQueueDisplay is
// updated automatically via the onDequeued callback — no direct setState here.
expect(segment).toContain("tuiQueueRef.current?.consumeItems(queueLen)");
expect(segment).toContain("onSubmitRef.current(concatenatedMessage);");
expect(segment).toContain("onSubmitRef.current(concatenatedMessage)");
expect(segment).toContain("!dequeueInFlightRef.current");
expect(segment).toContain("queuedOverlayAction,");
});