Repository navigation
fix(native-chat): no saved outbox on the desktop; one send at a time, and the host owns what it accepted - #25959
Conversation
…aved outbox The desktop kept each structured chat's unsent messages in localStorage and sent them in order, so one message whose fate was unknown froze every later send, Retry dropped it silently, failed sends could not be discarded, and an offline chat could send hours later. The host already records every message and owns the queue; the window now only sends. - One in-memory sender for composer, launch prompts and messages sent from outside the chat. One send in flight per chat; a transport failure resends the same id for up to 30 s; a refusal that proves nothing was recorded puts the text back in the composer with the reason; a send that went out and was never answered shows an in-doubt line with Send again and holds nothing up. - The host's "unknown" rows get the same in-doubt line and Send again, from the journal, in every window. - A message an older build left in localStorage is never sent: the host's conversation outline decides what goes back to the composer, and the copy is deleted once that is saved.
…its timeouts The sender called the runtime RPC directly, skipping the per-method timeouts every other structured chat call gets. Only a remote host's request skips the compatibility check the sender already ran.
… no new row line The common pattern draws nothing extra on a message whose delivery is in doubt once its turn is over, and puts a failed send's text back in the composer with the reason. So a send nobody answered in time, or one a host answers in a way that proves nothing, goes back to the composer worded as unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt phase, Send again, and its strings. A host's made-up row for an id its journal lost now reads as unconfirmed, not as recorded, so that message comes back instead of vanishing.
…send-queue # Conflicts: # src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx # src/renderer/src/components/native-chat/NativeChatMessageRow.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx # src/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.ts # src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx # src/renderer/src/components/native-chat/use-structured-agent-session.ts
…p, and keep remote images - A normal tab close lets sends on their way settle; what the host never took goes back to the conversation's draft. Only a cancelled launch or a worktree purge drops them. - After any Stop, a send already on its way is never sent again under its id: a doubtful answer, or a resend that was due, hands its text back worded as unconfirmed. - A returned image keeps the SSH connection it lives on; the connection never goes to the host. - An older build's saved message the host recorded and then rejected is left to the host's own row, never handed back.
…out, and a kept card is the card's A Stop that landed while a same-id resend was being readied (its timer fired, its request not out yet) let that resend go out after the Stop. The sender now tracks whether a request is awaiting its answer: a Stop hands back every send between attempts as unconfirmed, lets one whose request is out settle from its answer, and never issues a request after it. A normal tab close withdraws the same way instead of doing nothing, so nothing more goes out and nothing is dropped. A send the host rejected but kept as a card (keptAsQueuedMessageId) is the card's, from its reply or the journal, and never goes back to the composer. Adds the freeze tests: a send whose fate is unknown holds later sends no longer than its deadline, and one the host can neither confirm nor deny releases the next at once.
…proven Ports the send-answer proof contract. A call the host turned away before running it (method_not_found, invalid_argument, unauthorized) proves only that this request wrote nothing, so it reads as never sent on a first attempt only; on a resend an earlier attempt may have landed, and it goes again under the same id. A resent id the host says was already used for other content (messageIdReused) proves nothing either, like an expired or conflicting id. Pins the rest of the contract: any row the host returns is its own, a thrown error is no answer whatever its code, and an older build's refused, held or outlived-Stop copy is handed back once and never sent.
…ttachment type Keeps NativeChatStructuredSession.tsx within the file length limit.
A kept send's row is a rejection that was never a Stop's, so the sender already reads it as recorded, from its reply or the journal. The test that pins it stays; the two extra checks only covered a kept row that is also a withdrawal, which the host never writes.
…er the sender calls The sender sends through callStructuredAgentSession, but these launch tests replaced that module with a factory that answered nothing (and still named a probe that no longer exists), so every launch prompt resent until its 30 s deadline and the tests timed out. Each factory now hands sends to the runtime RPC mock the tests already answer, and expectations of a local send no longer ask for the remote-only compatibility option.
…s attachment hook The worktree purge reaches the launch prompt, which hands text back, and the attachment hook's imports reach the store. A test that builds the real store behind a mocked one then waited on itself and hung. Handing back now writes images to the draft store directly, as the hook's helper did, and a test pins that each returned image keeps its SSH connection.
A chat with a send out took further messages into an in-memory line and sent them one by one. A message typed behind one in doubt then hit its own 30 s deadline and came back as not sent without ever going out. Now, as the common pattern does, a chat takes one send at a time: while it is out, Send is disabled and Enter leaves the text in the box. The sender refuses a second send instead of lining it up, so the 30 s deadline always runs from the send itself. A Stop or a tab close stops the one send: settled from its answer if its request is out, handed back as unconfirmed between attempts, or silently if it never went out. Notes sent from outside the chat while its send is out stay with their sender (not ready); a launch prompt that meets the person's own first message waits in the composer instead of being lost.
…s notes were cleared Notes sent from outside a chat clear once their message is recorded. A message the host recorded as pending and a Stop then withdrew came back only to its sender, which had already let go of it, so the text was lost. The chat's draft now takes it, as an earlier build's outbox did.
…d the chat for its launch prompt - A send whose checks failed before its request went out (an unreachable or incompatible host, an unreadable history) was tried again at once, about a thousand times a second for 30 s. Attempts are now paced by the attempts made, whether or not their request went out. - A host that refuses every resend by throwing (native chat turned off, a journal that won't open) gave back only "couldn't confirm". The host's reason now comes first, still without claiming not sent. - A launch's prompt now holds the chat's one send from the click, drawn as sending, so a message typed while the chat starts can't overtake it; it goes out once the chat exists, and a cancelled launch frees it. - Notes whose send nobody could confirm say so instead of "did not accept", and notes launched into a new chat let go of their text once that chat's composer holds it. - An open chat keeps drawing a recorded send until its row arrives, so a reply that beats the history no longer makes the message flicker out.
…a failed chat's message with its restart A message sent to a chat still starting went out at once to a host that had no record of the chat yet, read for a fence it could not get, and came back as not sent. A message sent to a chat whose start failed restarted it but no longer went with the restart. While a chat starts, Send stays off and Enter leaves the text in the box, as with a send already out. A text message sent to a chat whose start failed restarts it and goes as the restart's first message, through the same staged-prompt path a launch prompt takes: it holds the chat's one send slot, drawn as sending, until the chat exists, and comes back to the composer if the restart fails again. Notes wait while a chat starts and ride a failed chat's restart, keeping their text until it is sent.
…hand-offs; drop an unused clear - Pin which sends ask the host to queue, the moved rejection wording, and that a queue send whose card was handed off and then refused or withdrawn, or whose replay names a withdrawn card, is never handed back. - The send-at-most-once gate now says what the desktop promises after a reload: it never sends the id again, and hands the text back. - The legacy read names when it goes, and drops the notice clear nothing called.
… once what can't go out - Cleaning up a launch prompt released its send slot even after the prompt had gone out, which deleted the entry the sender keeps once the host records it. A launch prompt recorded as pending and then withdrawn by a Stop was lost from both the chat and the box, and an open chat dropped the new chat's first message before its row arrived. The slot now gives back only a reservation that was never sent. - A send stopped before its request went out by something trying again won't clear (this client and the server can't talk, or the host refused the history read) kept Send off for 30 s and then said Orca couldn't reach the agent. It now comes back at once with its own cause: not sent, since nothing went out, or unconfirmed if an earlier attempt did. Transport errors keep the paced retries.
Notes sent to a New agent whose start failed came back to the notes and also sat in the new chat's composer, so sending both delivered the text twice. The notes keep their text until it goes out (they hold it from the click), so the launch now says so and no composer gets a copy, on a failed start or a refused prompt alike. The tests that asserted the copy in the composer pinned the old double ownership and now assert the notes are its only owner. Notes that rode a failed chat's restart and ended unconfirmed now say Orca couldn't confirm them, as notes sent directly do, instead of that the agent did not accept them.
…ross a reload The desktop keeps a send's id in memory only: a send still unsettled at a reload or crash is not resent and not handed back. The coverage notes no longer credit the renderer tests with durable identity across a remount.
…brennanb2025/nc-host-send-queue - Queue UI (#24586) taken as main has it: paused header, Resume, Steer, clear-queue question, `queueSendsNext` working state, composer Resume and queue hold. - The chat's send answers 'queued' when the transcript draws it as a card (main's rule, read through the in-memory sends' own projection), so the composer moves the reader only for a bubble. - The failed-start send wrapper passes that answer through; the submit reveal hook drops the per-message Retry this branch removed (launch Retry still reveals). - The pending-send projection takes any send shape, so the node project's queued-card test does not reach the renderer store. - Deleted outbox files stay deleted; #24514's queued-card admission test is ported to the sends hook.
…follows the chat A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A message sent then went out, came back refused into the old conversation's draft with its line, and vanished when the chat moved on: the box and the line now belong to the new conversation. - A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and the text stays in the box, as for a send that is out. Notes sent from outside get "busy". - Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and what is left of its draft moves into the new conversation's draft, after anything there: when the composer's /clear settles, and again when the view moves.
…send is out While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out after the question opened.
Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too (text left in the box during a /clear stays with the old conversation), so it goes in its own change. Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from takes no send until the view leaves it.
- A view that unmounted while a /clear that moves the chat was out left the old conversation holding its sends until a reload: the late reply kept the hold for a view that was gone. The reply now releases it. - The local call for a conversation command has no deadline of its own, so a /clear that never answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one the remote call already uses), whichever comes first.
No conflicts. Main's host outage notice (#24898) merges into the chat view beside this branch's send state; nothing in main's 7 commits sends through the desktop's outbox.
Main's #25225 moved the composer transport into use-native-chat-structured-composer-transport.ts and Stop into structured-agent-session-stop-control.ts, both built on the saved outbox this branch removes. Both now read the in-memory sender: the transport sends through sendThroughLaunch (keeps image connectionIds, sendOut while starting), and Stop before the chat is published gives the launch's unsent text back to the composer (takeBackStructuredLaunchPrompts) and asks nothing of the host, as main's rule does for its outbox.
Takes main's notice card (#25835) as main has it; the chat's send errors reach it through controller.error. The outbox Stop-withdrawal module stays deleted: #25217 only exported its Retry test for the projection's awaitsRetry flag, and no in-memory send ever waits on a Retry. Main's opening-send test is ported to optimistic messages, minus its two waiting-on-Retry cases. Also: a launch text handed back by a Stop before publish (or a failed start) is guarded like the sender's hand-back, so a draft write that throws is reported and never skips the Stop; the Stop-before-publish cases for a notes launch, an already-sent launch and a throwing hand-back are tested; the legacy outbox comment no longer names a build number.
…k its copy in send order Pins, through the real chat hook, sends, legacy recovery and draft store, what opening such a chat does: the queued messages behind a message the host recorded in doubt come back to the composer once, in order, with the "not sent" or "couldn't confirm" wording; the chat is free to send under its read's fence; nothing happens while the chat has no fence here. The legacy reader now hands entries back in the order they were sent (queuedAt), as the older build's reader did, instead of array order.
Status: review loop and live QA done (final head d5a40b5)The problem. The desktop kept each native chat's unsent messages in a saved list (an outbox in What changes for you.
How it was reviewed. Four review rounds, an architecture review, a readiness checklist and five narrow checks (fresh reviewers each time), plus live QA on a second Mac. The design changed twice during review on the owner's rulings:
Fixed during review (each with a test that fails when the fix is removed):
Deferred (P3, not blocking):
Follow-ups:
Verified.
Not verified.
CI on d5a40b5 (final): all green on the first attempt: unit full https://github.com/stablyai/orca/actions/runs/37608569362, Mobile Checks, PR test LoC and the headless Node server. No reds. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe desktop structured-session send path now uses in-memory pending sends and issues one send request per message. The sender tracks request and journal outcomes, applies a 30-second deadline, and returns eligible unsent text to the composer. Launch prompts use the sender, and transcript projection uses optimistic-message records. The change also updates queue handling, Stop and clear-command behavior, delivery notices, legacy outbox recovery, and related tests and configuration. Priority: ➖ Normal Merge Risk: 🔵 Low · up to A queued follow-up can briefly appear missing after the composer clears it during a narrow turn transition. Delivery loss is not established, so this is a bounded visibility issue rather than a broad merge blocker. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
70bc2872-82a7-4657-9e57-3a9c61e87496
📒 Files selected for processing (195)
config/reliability-gates.jsoncconfig/scripts/pr-code-change-scope-cross-version-send-path.test.mjsconfig/scripts/pr-code-change-scope.mjsmobile/src/session/mobile-structured-agent-session-send.tsmobile/src/session/mobile-structured-queued-message-cards.tsmobile/src/session/mobile-structured-send-delivery.tsmobile/src/session/use-mobile-structured-send-with-outcome.tssrc/main/claude/claude-structured-history-window.test.tssrc/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-test-data.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.tssrc/main/runtime/rpc/methods/agent-launch-structured-prompt.test.tssrc/main/runtime/rpc/methods/agent-launch-structured-prompt.tssrc/renderer/src/components/native-chat/NativeChatComposer.tsxsrc/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsxsrc/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsxsrc/renderer/src/components/native-chat/NativeChatMessageRow.test.tsxsrc/renderer/src/components/native-chat/NativeChatMessageRow.tsxsrc/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsxsrc/renderer/src/components/native-chat/NativeChatRewindAction.target.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsxsrc/renderer/src/components/native-chat/agent-session-failure-words-text.test.tssrc/renderer/src/components/native-chat/native-chat-composer-draft-journal.tssrc/renderer/src/components/native-chat/native-chat-composer-draft-storage.tssrc/renderer/src/components/native-chat/native-chat-composer-types.tssrc/renderer/src/components/native-chat/native-chat-draft-cache.tssrc/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsxsrc/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsxsrc/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsxsrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.kept-card.test.tssrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.tssrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.test.tssrc/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.tssrc/renderer/src/components/native-chat/structured-agent-session-message-hand-back.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-hand-back.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.older-host.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.recorded-copy.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.rejected-in-place.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.tssrc/renderer/src/components/native-chat/structured-agent-session-message-sender.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-sender.tssrc/renderer/src/components/native-chat/structured-agent-session-operation-id.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-dispatch.owner-change.test.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-dispatch.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-entry-watch.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-retry.test.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-retry.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-storage.test.tssrc/renderer/src/components/native-chat/structured-agent-session-outbox-storage.tssrc/renderer/src/components/native-chat/structured-agent-session-pending-sends.tssrc/renderer/src/components/native-chat/structured-agent-session-queue-request.test.tssrc/renderer/src/components/native-chat/structured-agent-session-queue-request.tssrc/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.tssrc/renderer/src/components/native-chat/structured-agent-session-queued-cards.tssrc/renderer/src/components/native-chat/structured-agent-session-send-attempt.tssrc/renderer/src/components/native-chat/structured-agent-session-send-slot.tssrc/renderer/src/components/native-chat/structured-agent-session-stop-control.tssrc/renderer/src/components/native-chat/structured-agent-session-transcript-order.test.tssrc/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.test.tsxsrc/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.tssrc/renderer/src/components/native-chat/use-native-chat-held-queue-composer-send.tssrc/renderer/src/components/native-chat/use-native-chat-provisional-launch.tssrc/renderer/src/components/native-chat/use-native-chat-rewind.tssrc/renderer/src/components/native-chat/use-native-chat-structured-composer-send.tssrc/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.tssrc/renderer/src/components/native-chat/use-structured-agent-session-command-send-gate.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-command-write.tssrc/renderer/src/components/native-chat/use-structured-agent-session-context-usage.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-messages.tssrc/renderer/src/components/native-chat/use-structured-agent-session-mutate.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-mutate.tssrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-admission.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-failed-here.tssrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-fence.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-owner-change.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-ownership.tssrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-relaunch-hold.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-unconfirmed-probe.tssrc/renderer/src/components/native-chat/use-structured-agent-session-outbox-withdrawal.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.batch-churn.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.draft-hand-off.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.external-send.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.queue-delivery.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.rejected-reply.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.stop-parks-in-doubt.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.tssrc/renderer/src/components/native-chat/use-structured-agent-session-paired-runtime.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-prompt-cancel.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-prompt-state.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-question-answers.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-sends.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-sends.tssrc/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-stop.tssrc/renderer/src/components/native-chat/use-structured-agent-session-transport.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-transport.tssrc/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.clear-send.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.cut-turn-notice.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.rejected-card.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.rewind-outbox.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.rewind.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.tssrc/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.tssrc/renderer/src/components/native-chat/use-undelivered-structured-agent-session-outbox.tssrc/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsxsrc/renderer/src/components/tab-bar/QuickLaunchButton.tsxsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/active-agent-note-send-diagnostics.tssrc/renderer/src/lib/active-agent-note-send-result.tssrc/renderer/src/lib/agent-message-send.test.tssrc/renderer/src/lib/agent-message-send.tssrc/renderer/src/lib/agent-session-launch-plan.tssrc/renderer/src/lib/launch-agent-in-new-tab.tssrc/renderer/src/lib/launch-agent-structured-chat-guard.test.tssrc/renderer/src/lib/new-agent-prompt-outcome.test.tssrc/renderer/src/lib/new-agent-prompt-outcome.tssrc/renderer/src/lib/structured-agent-session-empty-chat-reuse.test.tssrc/renderer/src/lib/structured-agent-session-launch-after-failure.test.tssrc/renderer/src/lib/structured-agent-session-launch-callers.tssrc/renderer/src/lib/structured-agent-session-launch-cancellation.test.tssrc/renderer/src/lib/structured-agent-session-launch-close-race.test.tssrc/renderer/src/lib/structured-agent-session-launch-different-request.test.tssrc/renderer/src/lib/structured-agent-session-launch-empty-chat.tssrc/renderer/src/lib/structured-agent-session-launch-join-delivery.test.tssrc/renderer/src/lib/structured-agent-session-launch-message.tssrc/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.tssrc/renderer/src/lib/structured-agent-session-launch-prompt.test.tssrc/renderer/src/lib/structured-agent-session-launch-prompt.tssrc/renderer/src/lib/structured-agent-session-launch-request.tssrc/renderer/src/lib/structured-agent-session-launch.test.tssrc/renderer/src/lib/structured-agent-session-launch.tssrc/renderer/src/lib/structured-agent-session-paired-admission.tssrc/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.tssrc/renderer/src/runtime/structured-agent-session-accepted-send-capability.test.tsxsrc/renderer/src/runtime/structured-agent-session-accepted-send-capability.tssrc/renderer/src/runtime/structured-agent-session-client.tssrc/renderer/src/runtime/structured-agent-session-host-capability.tssrc/renderer/src/runtime/structured-agent-session-tab-retirement.test.tssrc/renderer/src/runtime/structured-agent-session-tab-retirement.tssrc/renderer/src/store/slices/ui-agent-send-target.test.tssrc/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.tssrc/shared/agent-session-conversation-command.tssrc/shared/agent-session-refusal-notice.test.tssrc/shared/agent-session-refusal-notice.tssrc/shared/agent-session-refusal-retry.test.tssrc/shared/native-chat-transcript-projection.tssrc/shared/structured-agent-session-draft-hand-off.test.tssrc/shared/structured-agent-session-draft-hand-off.tssrc/shared/structured-agent-session-message-projection.test.tssrc/shared/structured-agent-session-message-projection.tssrc/shared/structured-agent-session-outbox-admission.tssrc/shared/structured-agent-session-outbox-delivery.test.tssrc/shared/structured-agent-session-outbox-delivery.tssrc/shared/structured-agent-session-outbox-reconcile.test.tssrc/shared/structured-agent-session-outbox-reconcile.tssrc/shared/structured-agent-session-outbox-retry-hold.test.tssrc/shared/structured-agent-session-outbox-stop-withdrawal.tssrc/shared/structured-agent-session-outbox-unconfirmed-resend.tssrc/shared/structured-agent-session-outbox-withdrawn-send.test.tssrc/shared/structured-agent-session-outbox.queue-delivery.test.tssrc/shared/structured-agent-session-outbox.tssrc/shared/structured-agent-session-rejection-words.test.tssrc/shared/structured-agent-session-rejection-words.tssrc/shared/structured-agent-session-send-disposition.test.tssrc/shared/structured-agent-session-send-disposition.tssrc/shared/structured-agent-session-send-evidence.test.tssrc/shared/structured-agent-session-send-evidence.tssrc/shared/structured-agent-session-send-mutation.tstests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
💤 Files with no reviewable changes (55)
- src/renderer/src/components/native-chat/use-undelivered-structured-agent-session-outbox.ts
- src/renderer/src/components/native-chat/structured-agent-session-outbox-dispatch.owner-change.test.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.external-send.test.tsx
- src/shared/structured-agent-session-outbox-reconcile.test.ts
- src/renderer/src/components/native-chat/structured-agent-session-outbox-storage.test.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx
- src/shared/structured-agent-session-outbox-withdrawn-send.test.ts
- src/renderer/src/components/native-chat/use-structured-agent-session.rewind-outbox.test.tsx
- src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx
- src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx
- src/shared/structured-agent-session-outbox-retry-hold.test.ts
- src/renderer/src/components/native-chat/structured-agent-session-outbox-retry.test.ts
- src/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.stop-parks-in-doubt.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.rejected-reply.test.tsx
- src/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.ts
- src/renderer/src/components/native-chat/structured-agent-session-message-projection.recorded-copy.test.ts
- src/renderer/src/runtime/structured-agent-session-accepted-send-capability.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-withdrawal.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-admission.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.batch-churn.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-relaunch-hold.test.tsx
- src/shared/structured-agent-session-outbox.queue-delivery.test.ts
- src/renderer/src/i18n/locales/ko.json
- src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
- src/renderer/src/i18n/locales/fr.json
- src/shared/structured-agent-session-outbox-reconcile.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.queue-delivery.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.draft-hand-off.test.tsx
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-fence.test.tsx
- config/scripts/pr-code-change-scope.mjs
- src/renderer/src/components/native-chat/structured-agent-session-outbox-dispatch.ts
- src/shared/structured-agent-session-outbox-admission.ts
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-unconfirmed-probe.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-owner-change.test.tsx
- src/renderer/src/i18n/locales/en.json
- src/renderer/src/components/native-chat/structured-agent-session-outbox-retry.ts
- src/renderer/src/components/native-chat/structured-agent-session-outbox-entry-watch.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-ownership.ts
- src/shared/structured-agent-session-outbox-unconfirmed-resend.ts
- src/renderer/src/i18n/locales/ja.json
- src/renderer/src/i18n/locales/es.json
- src/shared/agent-session-refusal-retry.test.ts
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox-failed-here.ts
- src/shared/structured-agent-session-outbox-delivery.test.ts
- src/renderer/src/runtime/structured-agent-session-accepted-send-capability.ts
- src/shared/structured-agent-session-outbox-stop-withdrawal.ts
- src/shared/structured-agent-session-outbox-delivery.ts
- src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
- src/shared/structured-agent-session-outbox.ts
- src/renderer/src/components/native-chat/structured-agent-session-message-projection.older-host.test.ts
- src/renderer/src/components/native-chat/structured-agent-session-outbox-storage.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| [queueCapability, queueEnabled] | ||
| ) | ||
| const outboxController = useStructuredAgentSessionOutbox({ | ||
| const sends = useStructuredAgentSessionSends({ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Give the send hook the same isWorking that the transcript uses.
useStructuredAgentSessionSends decides whether send returns 'queued' by running pendingSendsOutsideQueuedCards with transportState.isWorking (Line 148). The transcript filters the same pending sends with transportState.isWorking || transportState.queueSendsNext (Lines 192-195). Assume queueSendsNext is true and no turn runs. Then send returns true, and the composer reveals the transcript for a bubble. The transcript hides that same queue-if-active entry because it treats the entry as a card. The reader is scrolled to the bottom, and no bubble is drawn there. The comment in use-structured-agent-session-sends.ts says "The same projection that draws the transcript decides it". The inputs differ, so the two decisions can differ.
Compute the combined isWorking before calling useStructuredAgentSessionSends, and pass it to that hook.
Proposed fix
+ // A queued send is a card, never a transcript bubble.
+ const isWorking = transportState.isWorking || transportState.queueSendsNext
const sends = useStructuredAgentSessionSends({
sessionId,
target,
fence: transportState.fence,
submissions: transportState.submissions,
queuedMessageIds,
queue,
historyLoaded: transportEnabled && state.status === 'ready',
- isWorking: transportState.isWorking,
+ isWorking,
stopping: stopControl.stopping
})
...
- // A queued send is a card, never a transcript bubble.
- const isWorking = transportState.isWorking || transportState.queueSendsNext…er comes back as not sent A managed server's update rotates its pairing, and this window's main process then answers the next call itself, before forwarding anything, with runtime_environment_changed. The send read that thrown answer as proving nothing, so the person was told Orca couldn't confirm a message that never left. It is now handed back as not sent, with the reason. Every other thrown answer still reads as unconfirmed once the request may have gone out.
…back with its file and why A paired server checks every stored file a message names when it admits it, and refuses the whole message before recording it when one has expired. The sender hands such a message back to the chat's draft, file included, with the refusal's words, whether or not a view shows the chat, so it can be removed and attached again. Ported from the saved-outbox test that came with attaching files to a structured chat on a paired server.
…nto brennanb2025/rsc-resume-reconnect #25652: each workspace is a bordered box drawn in the content area, a nested child's box sits inside its parent's, and rows have no dividers. Under a machine row everything a machine holds moves in one level while every checkbox stays in the outside column. The machine row is the header over its boxes, never boxed: the project's band in the content area, set off from the previous machine's boxes by twice the list's gap. Main via the base: the in-chat "why Orca stopped" row and Continue (#25675) and the desktop send layer (#25959). - Host: main's restart-resume contract module now also declares this branch's listed dismissal; the branch's own copy of that type is gone. continueInterrupted and the listed dismissal both stay. - Client: one options argument carries both the pairing fence and the compatibility skip; restartContinue keeps its 195 s. - Continue is hidden while a resume carries the chat on its own machine (resumes are per machine here), and the launch's resume decision is marked where main marks it: when this computer's resume names its chats, and when the launch read ends.
Main removed the desktop's saved outbox (#25959): the renderer's delivery notices now come from in-memory sends and the host's rows. PR A's start-row reading (a row keyed by its message speaks for it alone; otherwise any loaded row with the same failure, but a command's) is kept on the new shape, and the start-row test moves to the new API; its outbox-only and released-host-batch cases are gone. Main's new Claude sign-in prompt counts start-failure rows through their fact.
ELI5
In Orca's native chat, the desktop kept every message you sent in a saved list on your computer and sent the list in order. When one message's fate was unknown, Orca marked it "Message delivery is unconfirmed." and everything you typed after it waited behind it, forever, even after quitting and reopening Orca. Retry didn't free it.
Now the desktop doesn't keep a saved list. It sends one message at a time straight to the machine running the agent (the host), which already records and queues every message. Each message is sent once. If its answer is lost, or nothing answers within 30 seconds, the text goes straight back into your message box with a note to check the chat. Orca never sends it again on its own. Nothing is ever stuck behind a message whose fate is unknown.
What Changed
The problem.
localStorage) and sent them in order. A message whose outcome was unknown (an answer lost on a dropped connection, or a host that recorded it as "unknown" after a Stop whose interrupt failed) became a barrier: every later message waited behind it, across restarts. Retry resent the same record and changed nothing (fix(native-chat): a message whose delivery is in doubt no longer holds every later send #25028).What you see now.
The mechanism.
structured-agent-session-message-sender.ts) is the only path toagentSession.sendfor the composer, launch prompts and messages sent from outside the chat. It keeps at most one send per chat, in memory only, until the host's answer or its journal settles it.structured-agent-session-pending-sends.tsholds that state and the chat's last hand-back reason; the composer readssendOutto disable Send. A launch prompt, or a message sent into a chat whose start failed, reserves that one send from the click and goes out once the chat is published (structured-agent-session-send-slot.ts,lib/structured-agent-session-launch-message.ts).src/shared/structured-agent-session-send-evidence.tsreads its answer one of three ways: recorded (a row in any state, including rejected, withdrawn or unknown, or a queued card), not recorded (a refusal the host returned, or a call it turned away before running it) which hands the text back with the reason, or uncertain (a timeout, a dropped connection, a thrown refusal, "outcome unknown") which hands it back with "couldn't confirm… check the chat". A failure before the request goes out (the compatibility check, the history read) hands the text back at once as not sent, with its reason.stopStructuredAgentSessionSends) hands a send that has not left the window back to the draft, and it is never sent. A request already out settles from its answer, or at the 30 s deadline. A message the host recorded and a Stop then withdrew stays in the chat with its stop row (main's rule since feat(native-chat): keep a message a Stop took back on screen, with one stop row after it #25051). Only a cancelled launch, or purging a worktree whose launch is still in progress, drops a send.structured-agent-session-legacy-outbox.tsreads an older build's saved outbox once per chat, asks the host's conversation outline which messages it holds, hands back the rest, never sends, and deletes the copy once the hand-back is saved. Temporary: remove once builds that wrote it have aged out.Phone. The phone never had the desktop outbox: it sends each message directly to the host, which owns any queue, so a message in doubt holds nothing back there. Nothing changes on the phone.
Why
Differences from the common pattern
Linked Issue
No issue. Supersedes #25207 and #25028 (both closed with pointers here). Keeps main's #25051 behaviour as is: a stopped message stays in the chat with one stop row, and the box is untouched.
Visual Proof
Live checks in a hidden test build of Orca with a stand-in Codex agent (no real agent). Each heading names the commits its shots came from. The current head 330c8d7 is 7656657 with main 81ba5c3 merged in. On 330c8d7 these passed: normal send, Enter while a send is out, a first prompt from a new agent tab and from a desktop AI button ("Explain changes" on a commit), Stop before the chat has started, and a send during a slow /clear. The other cases passed on 7656657: dropped and hanging requests, Stop while a send is out, a tab closed mid-send, a failed start (twice), older saved messages (seeded, and a real upgrade from main), and the #25028 freeze with a relaunch.
Pressing Enter while an earlier message is still being sent
Sending while /clear is still running
This PR (330c8d7), after the clear: the new conversation's box is empty because the text is saved as the old conversation's draft; carrying it over is a separate follow-up.
Upgrading with messages left in main's saved outbox (same profile)
Stop with a refused interrupt, then two more messages (the #25028 freeze)
This PR (7656657), the same chat after quitting and reopening Orca: all four messages, no notice.
A failed chat that fails again when you send
Stop while a second message is still being sent (unchanged from main)
This PR only
330c8d7: "Explain changes" on a commit opens a chat whose prompt shows "Sending…" from the click; text typed while it starts stays in the box, and the prompt is the agent's first message.
330c8d7: the same for a first prompt from a new agent tab.
330c8d7: Stop while the first prompt waits for the chat to start; the prompt goes back into the box after the typed text and is never sent, even once the chat is up.
7656657: a send whose request hangs comes back to the box 30 s after Enter, with "Orca couldn't confirm…" above it; it is sent once, never again.
7656657: the tab was closed while "c7 second" was on its way; reopening the conversation shows it as a draft: "Not sent yet — press Send to send it."
7656657: an older build's saved message comes back to the box once; the one the host rejected stays only on its row; nothing is sent.
Windows (d5a40b5), Windows high spec
Windows, d5a40b5: a send whose request hangs comes back to the box 30 s after Enter with "Orca couldn't confirm…" above it; sent once.
Windows, d5a40b5: Stop while a new chat's first prompt waits for the start; the prompt goes back into the box after the typed text and is never sent.
Windows, d5a40b5: Enter while "c2 second" is still being sent leaves "c2 third" in the box.
Paired Orca server (Windows high spec), head 6afd0c6. The desktop client is the PR build. The chat runs on a headless paired Orca server, built from either this PR or main 80d4509 (the main this head merged).
addFromPairingCodeis refused ("A server named … already exists"), so no send raced a pairing changeServer PR, client PR: normal send to a chat on the paired server.
Server PR, client PR: the hanging send is back in the box with "Orca couldn't confirm…"; the server delivered it late, so it is also in the chat.
Server PR, client PR: re-pair attempts were refused, so every send simply went through (the "not sent" path was not reached live).
Server main 80d4509, client PR: normal send.
Server main 80d4509, client PR: the hanging send is back in the box with "Orca couldn't confirm…"; one request.
Attachments (Windows high spec), head 7df3e6e
Paired server, 7df3e6e: a dropped text file is uploaded to the server and sent as its server path with the message, in one request.
Local chat, 7df3e6e: a dropped image is sent with the message in one request.
Testing
Unit tests pin each rule above. Each was checked by removing its fix and seeing the tests fail: the fix(native-chat): a message whose delivery is in doubt no longer holds every later send #25028 freeze (an in-doubt send never given back), refusing a second send, Send staying disabled until the send settles, one request per send (nothing is ever sent twice), a Stop taking back a send still being readied while a send already out settles from its answer, a tab close not dropping a send, a host rejection not also handed back, SSH images keeping their connection, notes refused while busy, and old saved entries handed back once and never sent.
On the final head (330c8d7):
tc:node,tc:webandtc:clipass; 325 affected test files (4,802 tests) pass; localization, reliability gates and code-quality checks pass; CI is green on the first attempt. Mobile typecheck and test-typecheck ratchet pass.I manually tested these changes locally (live QA on a second Mac against main; screenshots above)
Automated tests added/updated, or explained why not below
Review
Review loop and live QA done; summary in the status comment: #25959 (comment)
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)