Skip to content

feat(coding-agent): edit queued messages in place and preserve the queue on interrupt - #838

Merged
alexzhang13 merged 12 commits into
mainfrom
snimu/queue-edit-v2
Aug 11, 2026
Merged

feat(coding-agent): edit queued messages in place and preserve the queue on interrupt#838
alexzhang13 merged 12 commits into
mainfrom
snimu/queue-edit-v2

Conversation

@snimu

@snimu snimu commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Replaces #633 with a ground-up redesign at roughly a third of the diff (+1,033/−89 vs +2,397/−696) and less than half the net production code.

What it does

  • Alt+Up / Alt+Down browse queued steering and follow-up messages newest-first, from the draft and back to it. The selected item loads into the editor for in-place editing.
  • Enter applies the edit as steering (moves follow-ups up); Alt+Enter applies it as a follow-up (moves steering down).
  • Submitting an empty edit deletes the item. Esc-esc exits browse mode and restores the draft instead of arming an accidental delete.
  • Ctrl+Alt+Up / Ctrl+Alt+Down reorder the selected item within its lane (modern and legacy macOS Option-as-Meta arrow encodings both supported).
  • Ctrl+C / Escape during a rollout abort the turn but preserve the queue: nothing pops into the editor, queued messages stay server-owned and visible, and draining resumes on the next successful edit or fresh submit.

Design

The key observation is that main already preserves the queue across aborts: requestAbort() suspends the input pump without clearing queued actions, and _prompt() resumes it on the next submit. The only reason Ctrl+C dumped the queue into the editor was one explicit restoreQueuedMessagesToEditor({ abort: true }) call. Interrupt is now a plain abort().

Mutations are addressed by (lane, index, expectedText), verified server-side against the same projection the session-action snapshot publishes. Same-text queue items are semantically interchangeable, so this gives compare-and-swap-equivalent safety with no ids, no revisions, and no snapshot schema changes — queue state on the wire stays plain strings.

  • AgentSession.mutateQueuedMessage(lane, index, expectedText, mutation)applied | rejected | invalid with delete, move, and replace mutations. Deletes settle agentMessageId delivery/completion outcomes (a deleted promptAndWait turn rejects instead of hanging). Replaces rebuild content and primary records, honor an image tri-state (undefined preserves server images, [] clears, list replaces), reject non-editable turns (accepted agent messages, injected custom-message turns), update the wake policy on lane flips, and resume queued work.
  • Client-side QueueSelection (85 lines) tracks the browse cursor and stashes the draft; on queue changes it keeps, retargets by text, or drops the selection while preserving the stashed draft.
  • Daemon wire: one mutate_queued_message request behind the queue_message_mutation capability (schema 14 → 15). Older daemons report unsupported and the UI shows "Queue editing requires a newer daemon". The legacy abort_and_clear_queue wire command is untouched for compatibility.
  • app.message.dequeue (restore-queue-to-editor) is superseded and migrated to app.message.navigateOlder; existing custom bindings carry over via the keybindings migration.

Not in this PR (dropped from the #633 approach)

Stable action ids, queue revisions/CAS plumbing, snapshot items[]/revision fields, client pause/resume state machinery (pause flags, generations, single-flight resume, interrupt-and-recall, ownership sentinels, checkpoint/reconcile), and image tri-state UI plumbing — all unnecessary under the queue-preserving interrupt semantics above.

Tests

  • agent-session-queue-mutation.test.ts: projection addressing, stale-address rejection, delete settlement, boundary moves, lane flips (both directions, wake policy), image tri-state incl. content/record rebuild, session-command validation, injected-turn rejection, and abort → edit → resume.
  • queue-selection.test.ts: browse order, boundary noops, sync keep/retarget/drop, draft stash survival across external drops.
  • interactive-queue-edit.test.ts: enter/alt+enter lane targets, empty-submit delete, edit restoration when a mutation is rejected after Enter cleared the editor, unsupported-daemon status, interrupt preserving the queue.
  • Updated interactive-mode-ctrl-c suite to the new interrupt contract; daemon protocol/round-trip coverage; keybinding migration; tui key-encoding tests.

npm run check green; 660 coding-agent tests across 17 suites green; 740 tui tests green.


Note

Medium Risk
Touches session queue delivery, abort/interrupt UX, and daemon protocol revision 15; mistakes could drop or mis-order prompts or strand suspended work, though extensive tests cover mutations and race cases.

Overview
Queued messages can be browsed and edited in place instead of dumping the whole queue into the editor. Alt+Up / Alt+Down walk steering and follow-up items (newest-first) while stashing the draft; Enter / Alt+Enter apply edits as steering or follow-up, and an empty submit deletes the selected item. Ctrl+Alt+Up / Down reorder within a lane (with TUI support for legacy Option-as-Meta arrow encodings).

Server-side mutateQueuedMessage (lane, index, expectedText, delete/move/replace) backs this via mutate_queued_message on the daemon (schema 15, queue_message_mutation capability). The interactive UI uses QueueSelection and serialized optimistic updates.

Ctrl+C during streaming now calls abort() only—the queue stays server-owned and visible; draining resumes on the next submit or queue edit. app.message.dequeue is migrated to app.message.navigateOlder.

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

Note

Add in-place queue message editing and preserve the queue on interrupt

  • Replaces the bulk dequeue-to-editor restore flow with in-place browsing and editing: Alt+Up/Down navigates queued messages, Enter/Alt+Enter applies edits as steering or follow-up, and an empty submit deletes the item.
  • Adds QueueSelection (queue-selection.ts) to track cursor position, stash/restore the editor draft, and reconcile selection when the server queue changes.
  • Adds AgentSession.mutateQueuedMessage (agent-session.ts) supporting delete, move (in-lane swap), replace (text/images), and lane flip (steering↔follow-up) with optimistic concurrency checks.
  • Propagates mutateQueuedMessage through the daemon protocol (schema revision 15, new queue_message_mutation capability) and both DaemonAgentConnection and InProcessAgentConnection.
  • Replaces the app.message.dequeue keybinding with four commands: navigateOlder, navigateNewer, moveEarlier, moveLater; existing user configs are migrated automatically.
  • Ctrl+C now aborts streaming and leaves queued messages server-side instead of restoring them into the editor.
  • Risk: queue mutations are serialized client-side but optimistic local patches may diverge briefly if a concurrent server event arrives between the request and its response.

Macroscope summarized eb16eac.

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 Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
@snimu
snimu requested a review from alexzhang13 August 7, 2026 11:10
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
Fixes OpenAI Responses 400 error 'reasoning without following item' by
skipping errored/aborted assistant messages entirely rather than filtering
at the provider level. This covers openai-responses, openai-codex-responses,
and future providers.

Removes strictResponsesPairing compat option (no longer needed).

Closes PrimeIntellect-ai#838
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
@snimu

snimu commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Both fixed in bb8ffe2:

  • Stale selection across session switches (High): resetCurrentSessionRenderState now resets queueSelection alongside connectionQueue. The stashed draft is discarded rather than restored — every editor draft is cleared by the same reset, so restoring a previous session's draft would be wrong too. Regression test drives a browse, resets render state, and asserts the next Enter is a fresh prompt.
  • Stale mirror after replace/delete (Medium): applied replace and delete mutations now update connectionQueue optimistically, exactly like the move path already did (same guard: only when the lane still holds expectedText at the index; the later queue event resyncs to the same state). Lane-changing replaces move the item to the end of the target lane, matching the daemon's moveQueued(..., length) semantics. Tests cover in-lane replace, delete, and cross-lane replace.

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

snimu commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in b082b76. The race is real: in-process connections emit session_action_update before the mutateQueuedMessage await resumes, and with duplicate-text items sync retargets the selection so the text guard alone passes and the patch double-applies.

Queue events always assign a fresh connectionQueue object (refreshConnectionQueue, the event handler, and the render-state reset all replace the object; nothing else mutates it), so both optimistic patch sites now also require this.connectionQueue === queueBefore — the object captured before the request. If any event landed meanwhile, the mirror is authoritative and the patch is skipped. Regression test reproduces the exact scenario (event-before-response with two same-text items, delete must not double-apply).

Same commit also trims review-fix bloat from the previous round: the session-switch coverage now extends the existing render-state reset tests instead of a duplicated 40-line harness, and the defensive ?. on queueSelection in the reset path is gone.

snimu and others added 8 commits August 10, 2026 20:20
…eue on interrupt

Browse queued steering/follow-up messages with Alt+Up/Alt+Down, edit the
selected item in the editor, and apply with Enter (steering) or Alt+Enter
(follow-up), flipping lanes when needed. Submitting an empty edit deletes the
item; Ctrl+Alt+Up/Down reorders it within its lane.

Ctrl+C/Escape now abort the active turn without clearing the queue: queued
messages stay server-owned and visible, and draining resumes on the next
successful edit or fresh submit (the input pump is already suspended by
requestAbort and resumed by _prompt on main).

Server: AgentSession.mutateQueuedMessage addresses items by
(lane, index, expectedText) against the same projection the snapshot
publishes, so no ids, revisions, or snapshot changes are needed. Deletes
settle agentMessageId outcomes; replaces rebuild content/records, honor an
image tri-state (preserve/clear/replace), reject non-editable turns, and
resume queued work. Daemon wire adds one mutate_queued_message request behind
the queue_message_mutation capability (schema 13 -> 14); older daemons report
"unsupported" and the UI shows a status message.

app.message.dequeue is migrated to app.message.navigateOlder; the
restore-queue-to-editor path is removed.
- Serialize queue mutations through a single chain so rapid Enter/reorder
  presses cannot race each other or send stale indices; surface rejected
  reorders as a status message.
- Guard editor writes after a mutation with an editor-text ownership check so
  a stale completion never clobbers newer typing, and restore the edit when
  the mutation throws (Enter clears the editor before onSubmit runs).
- Restore the stashed draft when the browsed queue item is consumed or
  removed externally, so Enter cannot resubmit stale queued text.
- Deduplicate repeated [image #N] markers in replace mutations.
- Use the configured app.message.followUp key in the browse header instead of
  a hard-coded alt+enter.
- Drop the dead clearEditor() wrapper and tolerate partial test harnesses in
  the onSubmit/handleFollowUp queue interception (fixes the 4509 CI shard).
… event arrives

Over the daemon, the mutation response can land before the corresponding
session_action_update, so a queued follow-up move would still address the
pre-move index and be rejected. On a successful move, swap the local queue
mirror optimistically and resync the selection; the later event settles to
the same state.
…edits optimistically

resetCurrentSessionRenderState now clears the queue selection and its
stashed draft, so Enter in a freshly switched session can never issue a
mutation addressed at the previous session's queue. Applied replace and
delete mutations update the local queue mirror immediately (like moves
already did), so an immediate re-browse sees the new text instead of a
stale expectedText that the daemon would reject.
…eady landed

In-process connections deliver the session_action_update before the
mutateQueuedMessage await resumes; sync can retarget the selection onto a
same-text item, so the text guard alone let the optimistic patch apply the
delete or lane move a second time. Queue events always assign a fresh
connectionQueue object, so patch only when the mirror is still the exact
object captured before the request.

Also folds the session-switch regression coverage into the existing
render-state reset tests instead of a duplicated harness, and drops the
defensive optional chaining that hid missing harness fields.
…sion 15

Main took revision 14 for the telemetry policy surface (a18809e), so the
queued-message-mutation gate and the advertised schema identity move to a
fresh revision 15 with the recomputed wire-shape digest.
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
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
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 3bdb038. Configure here.

Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
@alexzhang13
alexzhang13 merged commit 71ca6cf into main Aug 11, 2026
17 checks passed
@alexzhang13
alexzhang13 deleted the snimu/queue-edit-v2 branch August 11, 2026 02:09
@snimu snimu mentioned this pull request Aug 11, 2026
9 tasks
sethkarten pushed a commit that referenced this pull request Aug 11, 2026
Patch release. Bug fixes, small UX additions behind existing surfaces, and a
dependency consolidation; no breaking changes, per the no-major-releases
policy.

Contents since v0.7.1:
- #838 in-place queue editing (Alt+Up/Alt+Down browse, Enter/Alt+Enter apply)
  and queue preservation on interrupt
- #850/#851/#852 worker lifecycle truthfulness, timed-out stop finalization,
  and stale-registration self-heal (the "Session worker is not connected"
  family)
- #1226 Down Arrow stays in a nonempty prompt until the cursor reaches the end
- #767 independent expand/collapse for tool calls, a2a messages, and thinking
- #1135 agents view keeps expansion state when leaving and returning
- #647 login URL copy action
- #521 privacy-safe agent analytics with disclosure and opt-out
- #846 Homebrew ownership preserved on self-update
- #772 sent a2a messages show only message text when expanded
- #632 consolidated dependency updates (undici 7.29, biome 2.5.5, marked 18,
  typescript 7 dev-only, typebox 1.3, aws-sdk bedrock, vitest 4.1.10, et al.)
- #1132 stale Gemini test model update (test-only)

Missing changelog entries for #838/#850/#851/#852 are added under 0.7.2.

Lockstep bump across the root package and the four published packages;
example and private workspaces untouched. Lockfile updated surgically
(version fields and inter-package ranges only).
0oAstro pushed a commit to 0oAstro/fulcrum that referenced this pull request Aug 11, 2026
…eue on interrupt (PrimeIntellect-ai#838)

* feat(coding-agent): edit queued messages in place and preserve the queue on interrupt

Browse queued steering/follow-up messages with Alt+Up/Alt+Down, edit the
selected item in the editor, and apply with Enter (steering) or Alt+Enter
(follow-up), flipping lanes when needed. Submitting an empty edit deletes the
item; Ctrl+Alt+Up/Down reorders it within its lane.

Ctrl+C/Escape now abort the active turn without clearing the queue: queued
messages stay server-owned and visible, and draining resumes on the next
successful edit or fresh submit (the input pump is already suspended by
requestAbort and resumed by _prompt on main).

Server: AgentSession.mutateQueuedMessage addresses items by
(lane, index, expectedText) against the same projection the snapshot
publishes, so no ids, revisions, or snapshot changes are needed. Deletes
settle agentMessageId outcomes; replaces rebuild content/records, honor an
image tri-state (preserve/clear/replace), reject non-editable turns, and
resume queued work. Daemon wire adds one mutate_queued_message request behind
the queue_message_mutation capability (schema 13 -> 14); older daemons report
"unsupported" and the UI shows a status message.

app.message.dequeue is migrated to app.message.navigateOlder; the
restore-queue-to-editor path is removed.

* fix(coding-agent): address queue-edit review findings

- Serialize queue mutations through a single chain so rapid Enter/reorder
  presses cannot race each other or send stale indices; surface rejected
  reorders as a status message.
- Guard editor writes after a mutation with an editor-text ownership check so
  a stale completion never clobbers newer typing, and restore the edit when
  the mutation throws (Enter clears the editor before onSubmit runs).
- Restore the stashed draft when the browsed queue item is consumed or
  removed externally, so Enter cannot resubmit stale queued text.
- Deduplicate repeated [image #N] markers in replace mutations.
- Use the configured app.message.followUp key in the browse header instead of
  a hard-coded alt+enter.
- Drop the dead clearEditor() wrapper and tolerate partial test harnesses in
  the onSubmit/handleFollowUp queue interception (fixes the 4509 CI shard).

* fix(coding-agent): keep chained reorders addressable before the queue event arrives

Over the daemon, the mutation response can land before the corresponding
session_action_update, so a queued follow-up move would still address the
pre-move index and be rejected. On a successful move, swap the local queue
mirror optimistically and resync the selection; the later event settles to
the same state.

* fix(coding-agent): reset queue browsing on session switch and mirror edits optimistically

resetCurrentSessionRenderState now clears the queue selection and its
stashed draft, so Enter in a freshly switched session can never issue a
mutation addressed at the previous session's queue. Applied replace and
delete mutations update the local queue mirror immediately (like moves
already did), so an immediate re-browse sees the new text instead of a
stale expectedText that the daemon would reject.

* fix(coding-agent): skip the optimistic queue patch when the event already landed

In-process connections deliver the session_action_update before the
mutateQueuedMessage await resumes; sync can retarget the selection onto a
same-text item, so the text guard alone let the optimistic patch apply the
delete or lane move a second time. Queue events always assign a fresh
connectionQueue object, so patch only when the mirror is still the exact
object captured before the request.

Also folds the session-switch regression coverage into the existing
render-state reset tests instead of a duplicated harness, and drops the
defensive optional chaining that hid missing harness fields.

* fix(coding-agent): move queued message mutation to daemon schema revision 15

Main took revision 14 for the telemetry policy surface (a18809e), so the
queued-message-mutation gate and the advertised schema identity move to a
fresh revision 15 with the recomputed wire-shape digest.

* docs(coding-agent): record revision 15 in the schema history comment

* when switching sessions with a queued message, make sure it doesn't appear in the other queue

* fix(coding-agent): harden queued edit lifecycle

* fix(coding-agent): preserve failed queue edit drafts

* fix(coding-agent): close queue edit state races

* fix: stashed draft lost after edit

---------

Co-authored-by: Alex Zhang <alex.lx.zhang@gmail.com>
0oAstro pushed a commit to 0oAstro/fulcrum that referenced this pull request Aug 11, 2026
Patch release. Bug fixes, small UX additions behind existing surfaces, and a
dependency consolidation; no breaking changes, per the no-major-releases
policy.

Contents since v0.7.1:
- PrimeIntellect-ai#838 in-place queue editing (Alt+Up/Alt+Down browse, Enter/Alt+Enter apply)
  and queue preservation on interrupt
- PrimeIntellect-ai#850/PrimeIntellect-ai#851/PrimeIntellect-ai#852 worker lifecycle truthfulness, timed-out stop finalization,
  and stale-registration self-heal (the "Session worker is not connected"
  family)
- PrimeIntellect-ai#1226 Down Arrow stays in a nonempty prompt until the cursor reaches the end
- PrimeIntellect-ai#767 independent expand/collapse for tool calls, a2a messages, and thinking
- PrimeIntellect-ai#1135 agents view keeps expansion state when leaving and returning
- PrimeIntellect-ai#647 login URL copy action
- PrimeIntellect-ai#521 privacy-safe agent analytics with disclosure and opt-out
- PrimeIntellect-ai#846 Homebrew ownership preserved on self-update
- PrimeIntellect-ai#772 sent a2a messages show only message text when expanded
- PrimeIntellect-ai#632 consolidated dependency updates (undici 7.29, biome 2.5.5, marked 18,
  typescript 7 dev-only, typebox 1.3, aws-sdk bedrock, vitest 4.1.10, et al.)
- PrimeIntellect-ai#1132 stale Gemini test model update (test-only)

Missing changelog entries for PrimeIntellect-ai#838/PrimeIntellect-ai#850/PrimeIntellect-ai#851/PrimeIntellect-ai#852 are added under 0.7.2.

Lockstep bump across the root package and the four published packages;
example and private workspaces untouched. Lockfile updated surgically
(version fields and inter-package ranges only).
junhoyeo added a commit to junhoyeo/prime-agent that referenced this pull request Aug 12, 2026
Upstream PrimeIntellect-ai/prime-agent main, 14 commits since merge base
a18809e. Seven files conflicted; resolutions:

daemon-protocol.ts - both sides independently shipped DAEMON_SCHEMA_REVISION
15 for different features (this fork: attach ownership / wasAttached; upstream:
mutate_queued_message, then 16 for the "stopping" workerState). Kept all three
features and moved this fork's change to revision 17. DAEMON_SCHEMA_ID is a
digest of the wire-type slices, recomputed rather than hand-written; it matches
upstream's because wasAttached lives outside those slices.

daemon-supervisor.ts - both sides independently implemented pid-reuse identity
verification. Took upstream's (this.processIdentity / isWorkerProcessAlive with
throttled recheck) because upstream's new daemon-supervisor-monitor tests encode
it, and dropped this fork's parallel helpers, folding workerMightBeAlive into
isWorkerProcessAlive, which has the same "unknown identity counts as alive"
semantics.

interactive-mode.ts - upstream's server-side queue preservation on interrupt
(PrimeIntellect-ai#838) supersedes this fork's restoreQueuedMessagesToEditor; kept this fork's
refine-command abort condition, which upstream has no equivalent for. Kept
syncWorkingLoader; dropped the duplicate updatePendingMessagesDisplay that
replaceConnectionQueue already performs.

models.generated.ts - regenerated from the merged generator rather than
hand-merged, since upstream's change was to the curated
PRIME_INFERENCE_FEATURED_MODELS input.

CHANGELOGs - this fork's entries stay under [Unreleased]; upstream's ship under
[0.7.2].

Constraint: packages/ai keeps this fork's decoupled build (no generate-models); upstream still regenerates during build
Confidence: medium
Scope-risk: broad
Not-tested: 11 failures remain under investigation against a develop baseline
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