Repository navigation
fix(#241): clear stale Codex queued prompts via idle-convergence reconciliation - #304
Merged
Merged
Conversation
…nciliation Codex queued follow-up prompts (runtime.queuedMessages) are local UI placeholders that normally clear when the matching committed rollout user row is ingested. If the rollout tail stalls or drops that row, the queued item sticks forever even after the session goes fully idle. The existing shouldClearIdleCodexQueuedMessages invariant already covers the all-idle condition, but it was only ever evaluated at three edge sites (onSessionProcessState, onSessionSemanticEvent, bootstrap_complete). Idle for Codex is assembled from several channels, and when the LAST idle signal arrives via a channel that doesn't re-run the invariant, the queue is never reconciled and the QueueStrip stays stuck. This converts the fix from edge-triggered to convergence-triggered: the existing ~1s ghost-orphan sweep timer now also re-evaluates the queue invariant per session as a backstop. To avoid clearing during the sub-second gap between a turn_completed and the next queued follow-up's turn_started, the sweep only acts once idle has been STABLE past a new QUEUE_IDLE_RECONCILE_MS quiet window (newest of phaseChangedAt / lastJsonlEntryAt). The three edge-site clears remain the fast path. Also adds an optional providerReportsPendingQueue gate to the invariant (default false; documented hook) so a future Codex server-side queue signal can veto clearing, and so Claude's provider-owned queue is never touched by this Codex-scoped reconciler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Codex queued follow-up prompts in
runtime.queuedMessagesare local UI placeholders. They normally clear when the matching committed rollout user row is ingested (thecodexPromptsMatchForOwnershipownership filter). When the rollout tail stalls or drops that row, the queued item sticks forever — and the QueueStrip stays visible even after the session is fully idle (#241).The existing
shouldClearIdleCodexQueuedMessagesinvariant (queueInvariants.ts) already encodes the correct all-idle condition, but it was only ever evaluated at three edge sites:onSessionProcessState,onSessionSemanticEvent, andbootstrap_complete. Idle for Codex is assembled from multiple channels (process-state, semantic phase transitions, JSONL tail). When the last idle signal arrives through a channel that doesn't re-run the invariant, nothing reconciles the queue and it sticks.Fix: edge-triggered → convergence-triggered
The three edge-site clears remain as the fast path. This PR adds a backstop in the existing ~1s
GHOST_ORPHAN_SWEEP_MSsweep timer (useIpcSubscriptions.ts): after the ghost-GC pass, it re-evaluates the same queue invariant per session and clears a stale Codex queue when idle has converged. The two passes compose into a singlesetRuntimespatch per tick (no earlycontinuebetween them).Quiet-window safety gate
A timer backstop risks clearing during the sub-second gap between a
turn_completed(everything momentarily looks idle) and the next queued follow-up'sturn_started. To prevent that, the sweep only clears once idle has been STABLE past a newQUEUE_IDLE_RECONCILE_MS(2000ms) quiet window, measured from the newest ofphaseChangedAt/lastJsonlEntryAt. This makes the clear convergence-triggered: wait for signals to quiesce, then reconcile.Provider gate protecting Claude
shouldClearIdleCodexQueuedMessagesgains an optionalproviderReportsPendingQueue?: boolean(defaultfalse, documented hook) that is AND-ed into the result. Codex emits no such signal today, so every caller passes the default. The existingprovider === 'codex'clause keeps the whole mechanism Codex-scoped — Claude's provider-ownedqueue-operationqueue is never touched by this lifecycle reconciler.Invariant (must hold)
A Codex queued item is cleared ONLY when, after idle has been stable past the quiet window, ALL of:
provider === 'codex',queuedMessages.length > 0,processActive === false,streamPhase === 'idle',awaitingAssistant === false, and no provider-reported pending queue. NEVER for Claude, never while any live signal is present, never before the quiet window elapses.Verification
npx tsc -p tsconfig.web.json --noEmit— zero new errors in touched files.queue_idle_reconcilefeed-debug entry confirms the clear).🤖 Generated with Claude Code