Skip to content

fix(coding-agent): single-source the interactive queue state - #1842

Merged
xeophon merged 2 commits into
mainfrom
snimu/tui-queue-single-source
Aug 29, 2026
Merged

fix(coding-agent): single-source the interactive queue state#1842
xeophon merged 2 commits into
mainfrom
snimu/tui-queue-single-source

Conversation

@snimu

@snimu snimu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The interactive queue was stored three times: the core session's action store (authoritative), the TUI's connection snapshot, and a second TUI-local mirror that optimistically re-applied every mutation. Keeping the copies aligned required identity guards, a sync() pass, and a text-based indexOf fallback — and that fallback had a real bug: with two identical queued messages, editing the later one could silently edit the first.

The fix

  • The TUI now reads the queue from one place (the connection snapshot of the session's actions). The local mirror, optimistic patching, identity guards, sync(), and the indexOf fallback are deleted.
  • Mutations are validated server-side against the original (lane, index, text) tuple, so an edit can never be redirected to a different item with the same text.
  • After an applied move, the selection re-derives deterministically from the refreshed canonical queue (the moved item is at index±1) — no text retargeting, no sync machinery.
  • Queued mutations resolve their target when their turn in the serialized chain begins, so consecutive moves and move-then-edit chains stay correct.

Net −167 lines in the first pass; the review round added the deterministic selection re-derivation (+69/−15).

How it's verified

Reviewer independently simulated server semantics against the real prototype methods: consecutive moves, move-then-edit, rapid unawaited keypresses, duplicate-text edits, selection reset races, and external queue shrink — all correct or safely rejected. New stateful regression asserts a 2→1→0 move/edit chain lands with the right indices. tsgo/biome/focused suites green (86 tests); repo-wide check green. Two-model implement/review loop, approved on second pass.

Stacked on #1702 (test the whole stack at the leaf; merge base-first).

Note: intentionally no Linear ticket for this cleanup stack, so that check stays red.


Note

Medium Risk
Changes interactive queue browse/edit/move behavior and session resync timing; fixes a duplicate-text mis-edit bug but regressions in queue UX would be user-visible during streaming sessions.

Overview
The interactive TUI no longer keeps a separate connectionQueue mirror or calls getQueue on refresh. Queued steering/follow-up messages are read only from connectionState.sessionActions via getConnectionQueue(), and the pending-queue UI updates from that snapshot on session events and resyncs (including a post-subscribe getState re-sync after rebind).

Queue browsing/editing now targets the exact (lane, index, text) tuple held in QueueSelection, instead of indexOf text matching. QueueSelection.sync() is replaced by refreshAt(), which resets browse mode when that tuple is no longer at the expected index—fixing mistaken edits when duplicate text appeared in the queue. pendingQueueMove blocks browse/refresh races while a reorder is in flight; refreshQueueSelectionFromState keeps the editor aligned when session_action_update arrives.

Move/replace/delete still go through the daemon mutateQueuedMessage chain, with slimmer local patching when the response beats the queue event. Tests and harnesses are updated to connectionState.sessionActions and add regressions for event ordering, duplicate text, and stale selection after failed moves/edits.

Reviewed by Cursor Bugbot for commit 6e277a8. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Single-source interactive queue state in InteractiveMode and fix duplicate-prompt targeting

  • Removes the local connectionQueue mirror from interactive.InteractiveMode and reads queued messages directly from connectionState.sessionActions via getConnectionQueue(), eliminating desynchronization between the local copy and server state
  • Replaces text-based QueueSelection.sync() with refreshAt(queue, lane, index, expectedText) and refreshQueueSelectionFromState(), so selection is reconciled by exact (lane, index, text) tuple and dropped on any mismatch instead of retargeting by text
  • Rewrites moveQueueSelection and applyQueueSelection to target the exact selected tuple even when duplicate texts exist, restore editor text on mutation failure, and add a pendingQueueMove flag that blocks browsing during in-flight moves
  • Behavioral Change: QueueSelection.sync(queue) is removed and replaced by refreshAt; any out-of-tree callers relying on sync or the connectionQueue field will break. The old text-retargeting selection behavior is gone — selections now drop on mismatch rather than silently retargeting

Macroscope summarized 6e277a8.

Linear: ENG-5649

Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 20ed314. Configure here.

Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
…failed edits

- rebindCurrentSession: re-sync sessionActions after resubscribing; a
  session_action_update fired in the unsubscribed gap was lost forever.
- browseQueueSelection: block browsing while a queue move RPC is pending,
  matching the pendingQueueEdit guard, so the move response cannot reselect
  the original item under newly browsed editor text.
- moveQueueSelection: when the applied move's session_action_update has not
  landed yet (sessionActions reference unchanged), mirror the swap locally so
  the moved selection is not dropped against the pre-move snapshot; reconcile
  the selection in the error path.
- applyQueueSelection: reconcile the selection after the pending-edit flag
  clears, so a rejected/failed edit no longer leaves Enter targeting a row
  that was removed while events were suppressed.
@snimu
snimu requested a review from xeophon August 29, 2026 08:19
@xeophon
xeophon merged commit bab1242 into main Aug 29, 2026
23 checks passed
@xeophon
xeophon deleted the snimu/tui-queue-single-source branch August 29, 2026 08:52
PR9000 pushed a commit to PR9000/prime-agent that referenced this pull request Sep 1, 2026
…tellect-ai#1842)

* fix(coding-agent): single-source the interactive queue state

* fix(coding-agent): close queue-state races around rebind, moves, and failed edits

- rebindCurrentSession: re-sync sessionActions after resubscribing; a
  session_action_update fired in the unsubscribed gap was lost forever.
- browseQueueSelection: block browsing while a queue move RPC is pending,
  matching the pendingQueueEdit guard, so the move response cannot reselect
  the original item under newly browsed editor text.
- moveQueueSelection: when the applied move's session_action_update has not
  landed yet (sessionActions reference unchanged), mirror the swap locally so
  the moved selection is not dropped against the pre-move snapshot; reconcile
  the selection in the error path.
- applyQueueSelection: reconcile the selection after the pending-edit flag
  clears, so a rejected/failed edit no longer leaves Enter targeting a row
  that was removed while events were suppressed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants