Skip to content

daemon: command-response replies return staged cancel-queued images - #348

Merged
wheregmis merged 3 commits into
mainfrom
devin/1790815661-cancel-queued-response
Oct 1, 2026
Merged

wheregmis merged 3 commits into
mainfrom
devin/1790815661-cancel-queued-response

Conversation

@wheregmis

@wheregmis wheregmis commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to #346: the wire had no way to return a command's result, so a remote cancel-queued restored only the staged text (recovered from the optimistic echo) and silently dropped staged images. This adds a point-to-point request/reply channel so CancelQueuedMessage can deliver the full staged payload back to the caller, plus a journaled recovery event so a disconnect can't eat the answer.

Wire (threadlane-protocol) — new frame shapes alongside the existing ones:

  • CommandRequest { request_id, command } — a SessionCommand sent as {"request_id": N, "command": {...}} instead of bare. Disjoint from a bare command by construction (no top-level type tag), and a protocol test asserts neither shape deserializes as the other.
  • CommandReply { request_id, result: Result<CommandResponse, String> } — sent as {"response": ...} on the requesting connection only: never journaled, never broadcast, so ?since= replays don't re-deliver it.
  • CommandResponse is the payload: Ack (every command that returns nothing) and CancelledQueuedMessage { session_id, entry_id, text, images }.
  • SessionEvent::QueuedEntryCancelled { session_id, entry_id, request_id, text, images } — journaled on every successful cancel with the same payload: the recovery path when a reply is lost to a disconnect (a replayed tail re-delivers it and the requester correlates by request_id), and how every client learns the entry went away.
  • SessionEvent::CommandResult { request_id, result } — synthesized by client transports for the requester; never appears in a journal tail.

Daemon — DaemonCore::dispatch returns Result<CommandResponse, String> (DaemonError broadcast unchanged); dispatch_with_request_id additionally threads the caller's request_id into the journaled QueuedEntryCancelled. dispatch_inner short-circuits CancelQueuedMessage to work_handle.cancel_queued_entry, mapping the staged (text, images) into the response; all other commands funnel through dispatch_effect → Ack. server.rs tries the CommandRequest parse first; on success it dispatches and sends the {"response"} frame back down the same connection's ordered outbound channel (reply_tx, a clone of out_tx).

Client — DaemonClient gains command_request(CommandRequest) -> Result<CommandResponse, String>; caller-chosen request_id correlates the reply. LocalDaemon just returns the dispatch result. RemoteDaemon keys request_id → PendingRequest in pending_requests: a reconnect fails every waiter except replayable ones (journaled-payload commands), which get one reconnect for the tail replay to resolve them via QueuedEntryCancelled; a 30s timeout fails callers whose reply will never arrive. Replies and journaled recovery both resolve the same waiter.

App — AppState gains a next_command_request_id counter and pending_queued_restores: HashMap<u64, PendingQueuedRestore>. The queued row's edit button now calls edit_queued_message (new sibling of cancel_queued_message, which keeps fire-and-forget for plain removal): it parks the intent, issues the cancel as a CommandRequest, and the reply re-enters via stream_tx as CommandResult — handled inside drain_chat_stream like every other daemon outcome. Either a CommandResult or a journaled QueuedEntryCancelled resolves the intent through apply_queued_cancel_restore: requested_composer_inserts gains a session-scoped insert (RequestedComposerInsert.session_id) that lands in the composer once that session is active — retained across a session switch rather than dropped or delivered to a foreign composer (text only if the echo hadn't already restored it).

Validation

  • cargo check -p threadlane-gpui clean; git diff --check clean.
  • cargo nextest run -p threadlane-protocol -p threadlane-client -p threadlane-daemon -p threadlane-ui-state: 154/154 pass, including new command_request_and_reply_round_trip_through_json (shape disjointness + all reply variants), remote_command_request_round_trips (real server: dispatch error resolves the caller's Err AND still broadcasts DaemonError; GetProjectState → Ok(Ack)), remote_edit_queued_message_reply_survives_a_session_switch, and remote_edit_queued_message_recovers_via_journaled_cancel_event (journal-tail recovery path).
  • cargo nextest run -p threadlane-ui-chat: 123/130 — the 7 failures (composer_at_completion_escape_dismisses_and_keeps_query, queued_panel_tracks_active_messages_and_generation, etc.) fail identically on main in this environment.

Link to Devin session: https://app.devin.ai/sessions/e1a52aee996148b6a500b6a489bcb5fa
Open in Devin Desktop: https://app.devin.ai/desktop/session/e1a52aee996148b6a500b6a489bcb5fa?variant=devin
Requested by: @wheregmis

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T01:04:52.059818Z 747efcc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 45 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 2117ca06-7563-40cf-82d7-9537d1c7524f

📥 Commits

Reviewing files that changed from the base of the PR and between 3fa7bab and abe4941.

📒 Files selected for processing (6)
  • crates/threadlane-client/src/remote.rs
  • crates/threadlane-daemon/src/core.rs
  • crates/threadlane-daemon/src/server.rs
  • crates/threadlane-protocol/src/daemon.rs
  • crates/threadlane-ui-state/src/app_state.rs
  • crates/threadlane-ui-state/src/tests.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 96d8e07b-6103-4aca-838c-f4cf2bd300ac

📥 Commits

Reviewing files that changed from the base of the PR and between 80b95d2 and 3fa7bab.

📒 Files selected for processing (15)
  • crates/threadlane-client/src/lib.rs
  • crates/threadlane-client/src/local.rs
  • crates/threadlane-client/src/remote.rs
  • crates/threadlane-client/tests/ws_roundtrip.rs
  • crates/threadlane-daemon/src/core.rs
  • crates/threadlane-daemon/src/lib.rs
  • crates/threadlane-daemon/src/server.rs
  • crates/threadlane-daemon/src/types.rs
  • crates/threadlane-protocol/src/daemon.rs
  • crates/threadlane-ui-chat/src/view.rs
  • crates/threadlane-ui-right-panel/src/browser/view.rs
  • crates/threadlane-ui-state/src/app_state.rs
  • crates/threadlane-ui-state/src/controller.rs
  • crates/threadlane-ui-state/src/tests.rs
  • crates/threadlane-ui-state/src/types.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


Summary by CodeRabbit

  • New Features
    • Editing a queued message now restores its staged text and image attachments to the composer.
  • Bug Fixes
    • Composer content requested for a specific session now waits until that session is active, preventing it from appearing in the wrong session.
    • Content without a session association, such as browser annotations or content restored after a failed send, can still appear in the visible composer.
    • Restoring queued-message content avoids duplicating text that has already returned to the composer.

Walkthrough

The change adds request-correlated command replies to the daemon protocol and clients. Queued-message cancellation can return staged text and images. The UI uses those responses and session-scoped composer inserts to restore queued message content.

Changes

Command replies and queued-message editing

Layer / File(s) Summary
Command response contract and dispatch
crates/threadlane-protocol/src/daemon.rs, crates/threadlane-daemon/src/core.rs, crates/threadlane-daemon/src/{types.rs,lib.rs}
The protocol defines request, reply, and response types. Daemon dispatch returns a response, including staged text and images for queued-message cancellation.
WebSocket request handling
crates/threadlane-daemon/src/server.rs
The server accepts request envelopes and sends results to the requesting connection. Bare commands remain supported.
Client request correlation
crates/threadlane-client/src/{lib.rs,local.rs,remote.rs}, crates/threadlane-client/tests/ws_roundtrip.rs
The client API adds command_request. The remote client matches replies to pending requests and reports send or connection errors. Tests cover successful replies and dispatch errors.
Queued message editing and composer restoration
crates/threadlane-ui-state/src/{types.rs,app_state.rs,controller.rs,tests.rs}, crates/threadlane-ui-chat/src/view.rs, crates/threadlane-ui-right-panel/src/browser/view.rs
Queued-message edits use correlated cancellation requests. Matching replies restore staged images and, when needed, text. Composer inserts can wait for their associated session.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant UI
  participant RemoteDaemon
  participant DaemonServer
  participant DaemonCore
  UI->>RemoteDaemon: Send correlated cancellation request
  RemoteDaemon->>DaemonServer: Forward request envelope
  DaemonServer->>DaemonCore: Dispatch cancellation
  DaemonCore-->>DaemonServer: Return cancelled message content
  DaemonServer-->>RemoteDaemon: Send request-correlated reply
  RemoteDaemon-->>UI: Deliver command result
  UI->>UI: Restore composer content for the matching session
Loading

Merge Risk: ⚪ Minimal · up to 3fa7b

The queued-message restoration changes are mergeable after normal checks. Session-scoped replies survive switching sessions; the annotation-routing concern is pre-existing and unchanged.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 3fa7b

Replies remain confined to the requesting connection and preserve the existing authentication checks. The main concern is client/daemon version skew: an older daemon rejects the new edit request while the client removes the queued row, potentially leaving recalled work eligible to run.

Retained concerns

  • Medium · reliability · inferred: A new remote client attached to the base daemon sends queued-message edits as CommandRequest envelopes that the older server rejects without executing cancellation. The client nevertheless removes the optimistic queue row and reports “Queued message removed.” The older server’s uncorrelated error does not resolve the pending request, whose wait has no deadline. Under mixed-version operation or daemon rollback, agent-directed work intended to be recalled can therefore remain queued and eligible for execution. Continued support for bare commands does not protect this newly enveloped edit path.
Security review details

Security Blast Radius

  • observed — The new sensitive response contains one cancelled queue entry’s text and images. It is sent through the requesting connection’s outbound channel, not the broadcast/journal path, and the remote client resolves its local waiter rather than distributing the reply to event subscribers.

Trust Boundaries and Controls

  • observed — Both frame shapes pass through the same WebSocket handshake, which rejects browser Origin headers and checks the bearer token when configured. Request handling invokes the same core dispatch as bare commands and preserves connection-owned terminal cleanup bookkeeping.

Resilience and Maintainability Implications

  • inferred — The mixed-version concern affects containment of agent-directed work, not merely response availability: rejection of the envelope prevents cancellation itself, while optimistic UI removal can suggest that the queued work was recalled.

Hardening Proposals

  • proposed — Establish request/reply capability before enabling remote edit, and preserve a visible unresolved cancellation state when dispatch cannot be confirmed. Bound the wait rather than treating an uncorrelated error as successful removal.
  • proposed — Document and enforce global session-ID uniqueness, or carry the originating project/session-file identity through pending restores and composer inserts. Composer drafts use project plus session identity, whereas delayed restore selection checks only session ID; copied-project uniqueness remains unproven.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.89% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 15 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the request/reply channel, staged image restoration, daemon and client changes, application behavior, and validation results. It directly matches the changeset.
Title check ✅ Passed The title clearly identifies the main change: command-response replies return staged images for cancel-queued operations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit sends a request down the wire,
A reply brings the queued words entire.
The pictures hop back, the session stays true,
The composer keeps each draft in view.
I twitch my nose: “A neat exchange!”

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 747efcc764

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/threadlane-ui-state/src/app_state.rs Outdated
Comment thread crates/threadlane-client/src/remote.rs Outdated
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Good catch — fixed in 3fa7bab2. RequestedComposerInsert now carries a session_id: Option<String>: the cancel reply pushes a session-scoped insert unconditionally, and the chat view applies it only when that session is active (unscoped None inserts keep the existing apply-now semantics). So a reply landing while another session is on screen is retained until the user returns to the queued message's session instead of being dropped — or worse, appended to a foreign composer, which is what the active-session gate was preventing. New test: remote_edit_queued_message_reply_survives_a_session_switch.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Fixed in 3fa7bab2 — see comment below: session-scoped RequestedComposerInsert retains the payload until the owning session is active again.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Fixed in abe49413, going with option 1: SessionEvent::QueuedEntryCancelled { session_id, entry_id, request_id, text, images } is now journaled on every successful CancelQueuedMessage dispatch — carrying the requester's request_id when the cancel came in as a CommandRequest.

On the client, RemoteDaemon marks CancelQueuedMessage waiters replayable: a disconnect keeps them parked for one reconnect instead of failing them, and a replayed QueuedEntryCancelled matching the request_id resolves the waiter with the same CancelledQueuedMessage payload the lost reply carried. A second disconnect fails them like any other request. AppState resolves the parked restore intent identically from either path.

Also bounded the wait while I was in there: command_request now times out after 30s instead of parking a caller forever if a reply never arrives.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

On the retained security-review concerns:

  • Unbounded wait / uncorrelated error — addressed in abe49413: command_request now fails the caller after a 30s timeout rather than parking forever, so a daemon that can't speak the envelope no longer leaves the pending restore open-ended.
  • Mixed-version optimistic removal — real residual: the client removes the queued echo optimistically before any dispatch confirmation, so against an older daemon the row says "removed" while the entry may still be queued daemon-side. That predates this PR for plain cancels (same optimistic pattern on bare commands); a capability handshake is the durable fix but is wire-surface work beyond this follow-up — worth an issue if version skew is a deployment reality.
  • Session-id uniqueness for the scoped insert — noted. pending_queued_restores and the scoped RequestedComposerInsert both key on session id alone, while composer_drafts keys on (work_dir, session_id). Session ids are per-daemon unique today; if copied projects can collide them in one app instance, carrying work_dir through the insert would harden it — flagged for a follow-up rather than expanded here.

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.

1 participant