Repository navigation
feat(native-chat): a half-typed chat message survives quitting Orca, with one draft per chat - #24461
Closed
brennanb2025 wants to merge 67 commits into
Closed
brennanb2025 wants to merge 67 commits into
brennanb2025 wants to merge 67 commits into
Conversation
…fts-survive-restart
…t only when it settles A clear that reached a composing pane left the sent text in that pane's box, and its next composed keystroke wrote the whole box back to the chat's draft: every other pane showed the sent text again and it was saved to disk, so a crash before settling restored it. Keystrokes now stay in the composing pane until it settles; a clear is still written at once, including when a held put-back rides along with it.
Drafts were deleted only by a 128-entry cap that dropped the least recently written, so an open chat's old draft could be deleted while a closed chat's lingered. A draft is now deleted when its structured session tab closes, when its terminal pane or tab closes, and when its worktree is purged, next to the outbox discards. The cap stays as a backstop for chats closed where this client never sees it, and never evicts a draft a view is showing.
…sing one blocks Send A saved draft kept image chips that pointed at OS temp files. A reboot, the OS temp cleanup, or Orca's own 7-day drag-copy sweep could delete them, and after a relaunch even a surviving image showed a blank preview because its read grant lived only in memory. Send then handed the agent a path to nothing. Images pasted or dropped (macOS drag copies) into a chat are now kept under <userData>/native-chat-attachments, one directory each, swept 30 days after they were made. That directory is a filesystem root, so restored previews load. Each restored chip is checked once; one whose file is gone is shown as missing and Send waits until it is removed. A check that cannot answer (an unreachable SSH host, a file outside Orca's roots) leaves the chip as it is. Terminal paste and terminal drops are unchanged.
… out of storage The saved draft and the outbox entry a send appends share localStorage, and the composer was cleared only after the append. A large saved draft could make the append fail, refusing the send. A plain message now clears the composer (written at once) just before the transport takes it, and is put back with restoreNativeChatDraftIfEmpty if the transport refuses or throws. Host commands still clear only once the host accepts.
…r sends Each tab loaded the saved drafts once, so after tab A sent a message, tab B kept showing it and its next keystroke saved it again. A tab now listens for another tab's change to a draft key and updates that draft and its views, unless it has its own write still pending for the key.
Views read the chat's draft while rendering but subscribed in a later passive effect, so a write landing in between was never shown there, and that view's next keystroke wrote over it. Both the text and the attachment hooks now re-read right after subscribing.
…red draft Follows the composing-pane change: a put-back made mid-composition is still kept when the view unmounts before settling, but the unsettled composition is not written.
With one chat in two panes, typing / in one pane mirrored the text into the other, whose caret moved with it, so both panes opened the picker and both reported it opened. Completions now derive only while this composer holds the keyboard focus (or nothing does).
… draft A chat over a terminal agent copies a launch draft (for example a linked issue) into the composer while the agent's input line holds the same text. That seed record lives in memory only, but the composer copy was saved, so after a relaunch the send no longer knew the line held the seed: it cleared one line and pasted the whole copy after the rest, duplicating it. The untouched copy now stays off disk until the user edits it, as before drafts were saved.
… one chat A pasted image still saving in one pane moved behind chips another pane added meanwhile, so images reached the agent in a different order from the one they were added in. The merge now keeps this view's order, pending chips in place, and appends chips it has not seen. Chip ids were the time plus a per-view counter, so two panes attaching in the same millisecond minted the same id and one chip was dropped from the shared draft. Ids are now random UUIDs.
…takes them Closing a tab now loads the drafts to discard them, which installs the cross-tab listener; a host without window events (store tests stub window) must not make that bookkeeping throw.
… as a workspace root getAllowedRoots lists the workspace roots, and its callers and tests depend on exactly that list. The chat attachment storage is now added only where paths are authorized.
…fts-survive-restart
…the chat's draft Keeping an adopted launch seed copy off disk lost a structured chat's launch prompt on relaunch (a structured chat has no input line holding it), and an edited copy of a terminal seed was still saved and sent on top of the line after a relaunch. A chat over a terminal agent now saves the seed it adopted with its draft, and on relaunch the launch record is seeded again as adopted, so a send still replaces the line and transcript resolution still clears it. Clearing the record forgets the saved seed. Structured chats save their launch prompt like any draft. Drafts are written either now or after the typing pause; callers always say which.
Each pane kept its own chip list and merged other panes' writes into it, so a paste still saving in one pane, plus an attach or a remove in another, left the panes and the saved draft in different orders, and the order sent depended on which pane pressed Enter. The chat's draft now holds one ordered list, pending chips included (left out when saving to disk), and add, remove, settle and clear are applied to it by chip id; every pane renders that list. Clipboard previews stay with the pane that pasted. The per-pane merge and the writer-token echo skip are gone.
The root lives in user data, which is already per user, but it reused the shared-temp check that demands owner and 0700 modes, so a restored root or one on a filesystem without modes failed every chat image paste. It now only has to be a directory, not a symlink.
…ore they are checked An image attached from outside Orca's folders (a Finder drag, the file picker) is granted for reading when it is attached, but that grant lives in memory. After a relaunch its preview was blank and its existence could not be checked, so a deleted original was never shown missing. Each restored local chip is now granted again, the way attaching grants it, before its check, and its preview waits for both.
…e, an SSH one does not
Removing or re-pairing a runtime environment purges its worktrees from this client, and the purge also deleted those chats' drafts. The sessions live on the host and come back when it publishes them again; losing the host binding is not proof a chat ended. That purge now keeps the drafts, and only the 128-draft backstop can drop them.
…fts-survive-restart
Previews stay with the pane that pasted, and were released only when their chip left the chat's list. Once that pane unmounted nothing held the blob URL any more, so a later remove from another pane could not free it. The pane now releases its previews when it unmounts.
…s' drafts Deleting a worktree from the app closed its structured chat tabs, which deletes their drafts, but dropped its terminal tabs wholesale without closeTab, so their chats' drafts and saved input-line seeds stayed on disk until the 128 cap pushed them out. The teardown now deletes them with the tabs. Removing or re-pairing a remote runtime still keeps drafts.
Composer drops copy into the chat attachment folder, but still demanded the owner-only root a shared temp folder needs, while a paste created the folder with ordinary modes. On macOS, after any chat paste, dragging a screenshot thumbnail onto a chat failed as storage that others can read. Orca now creates the folder owner-only, and both paths accept an existing one that is a real directory, not a symlink. Terminal drops keep the private temp root check.
A workspace image dragged into a chat on a remote runtime server was saved as a chip with no owner, so after a relaunch it was checked on this machine's disk, marked missing, and Send was blocked while the file existed on the server. Each chip now records where its file lives: this machine, an SSH host (its connection), or a runtime server. Only a file this machine can ask about is checked; a runtime server's file, or a chip saved before locations were recorded, is never marked missing. Older saved chips still load.
…nly broken files - The clear's delete now retries a Windows sharing violation, as the rename already did (#1507). - A write whose file op failed is kept per chat: the chat's next write supersedes it, and the will-quit drain retries it once, then drops it with a log line. The main process's own copy follows the newest write, so a reloaded window never gets back text a failed clear left behind. - Load keeps a newer build's draft files and files it could not read; it deletes only files that are not JSON or are broken version-1 drafts. Its deletions run in the chat's write queue and skip chats written this run. - A renamed temp file is not removed again, and the drafts folder and each file's stale-temp sweep happen once per run instead of on every write.
… wait cancels it - Both terminal send paths, typed and picked from the slash-command menu, go through one helper: clear, wait (capped) for the clear to be written, then write to the terminal. - The wait is tracked like a pending terminal write, so Stop, Escape or a pane switch during it cancels the send and puts the message back into the box, as a Stop before Enter would leave it. - A queued message taken back is deleted from the host only once its copy in the box is written (capped; deleted anyway on failure). - The web client reads drafts live when a chat first needs them, so it never misses another tab's send that happened earlier. - A clear that failed or outlasted the wait is logged with its chat and the reason. - Typing is also flushed on beforeunload.
…fts-survive-restart
…ges; no storage is not a failure - A terminal chat's first send cancelled during the clear's wait brings back the launch draft and its saved input-line seed it dropped at Enter: the agent's input line still holds the launch text, so the resend must replace it again. - A command picked from the menu puts the box's image chips back with it when cancelled, as the other send paths do. - The web client reports a browser with no storage as unavailable, which is not logged; only a write the browser refused (a full quota) is logged.
…fts-survive-restart
…er version is damaged - A reopened window (macOS keeps Orca running with none) re-registers only the draft handlers and keeps the same store, so a clear that failed earlier is still hidden from the window and still retried at quit; the quit drain covers every store. - At load, a JSON draft file with no numeric version, or an older one, is damaged and removed; only a newer build's file is kept.
…the message Live QA killed Orca 26-64 ms after Enter and lost the message 6 times in 6: the cleared draft was saved before the host had the message, and the outbox entry was still only in browser storage. Enter now empties the box and sends at once, with no wait. The chat's saved draft keeps the message until the host has it (a structured chat's outbox entry is gone; a terminal chat's write ran), and then the draft is saved as it is at that moment, so anything typed after Enter is kept. A refused send leaves the saved draft untouched. With several sends in flight on one chat, the last to land saves. This removes the wait for a saved clear before each send, its 250 ms cap on the send path, the cancel-during-the-wait handling, and the launch-draft and image put-back for a cancelled send.
…fts-survive-restart
Browser storage reaches disk lazily anyway, so holding the clear until the host has the message buys the web client no crash protection, and it shares its quota with the outbox: a large draft still saved could make the outbox append, and the send, fail. The web store now says it saves a send's clear at once, and the clear is written before the outbox takes the message. Desktop keeps saving it once the host has the message.
…er over an undelivered message - "The host has it" is now the host's ok answer to the send (pending, accepted or queued) or the journal showing the message, not the outbox entry leaving. A message the host holds while it waits for the provider no longer stays in the saved draft for the whole turn, where a quit and relaunch would restore an already delivered message. A message withdrawn back into the box, or dropped, still settles. - Sends on a chat are numbered: a send's save is skipped only while a later send still waits, so one held for Retry no longer stops every later send's clear from being saved. - No save for a message that was not delivered: a refusal that gives it a new id is followed to that id, a send dropped with its delivery unconfirmed keeps the draft copy, and a terminal write the agent rejected keeps it too.
…fts-survive-restart
…ctured send If the host's reply to a structured send never arrives (a remote host unreachable right after it took the message), the chat's saved draft kept the message, so a normal quit and relaunch restored a delivered message and Enter sent it twice. Closing the window (pagehide, beforeunload) now saves the draft of every structured send still waiting: the outbox, committed at a graceful quit, keeps any message that was not delivered, so nothing is lost. A held send now outlives only a crash. Terminal sends still save once the terminal write ran. The send holds move to their own module, and a chat that ends forgets its sends.
…fts-survive-restart
…sage until the host has it
…fts-survive-restart
…ed it over The host answers a send `pending` as soon as it records the message, but rejects every message it has not handed to the agent yet when Orca quits, and after a crash when the chat next opens. The saved draft let the message go on that `pending`, and every structured send still waiting was saved empty when the window closed, so a quit or a crash right after a send could lose the message (live QA case 12 and the crash-after-Enter runs). A structured send's draft copy is now released only once the host holds the message past a quit and a restart: the agent accepted it, the host handed it over (`pending` with a hand-over), or the host queued it as a card. An older host's `pending` still counts, since it hands over before it answers. Closing the window no longer saves the waiting sends' drafts empty. The outbox is untouched.
…fts-survive-restart
7 of 8 tasks
…epted it A send made while the agent works is handed into the running turn within milliseconds, and the draft copy was released at that handover. A handed-over message the agent never recorded is settled as never delivered when the chat reopens after a crash, and the transcript hides it unless the outbox still holds it, so a crash could lose it. The saved draft now keeps the message until the agent accepts it, the host queues it as a card, or it is withdrawn or dropped.
…fts-survive-restart
7 of 9 tasks
Contributor
Author
Review status: not ready yet, waiting on two host changesThe problem. Quitting Orca threw away a half-typed chat message. The first version of this PR saved drafts, but live testing found that killing Orca within about 3 seconds of pressing Enter brought the already-sent message back into the box (9 of 9 runs), so a second Enter would send it twice. The cause: browser storage writes to disk 5 or more seconds late. What changes for you (head d149ce0).
Fixed during review. 4 full review rounds, 6 narrow ones, 2 readiness passes and 7 live QA rounds. Every P0-P2 found is fixed. The main ones:
Not done yet (why this stays a draft).
Verified.
Not verified.
|
…x until the host holds it A crash between the host recording a send and the saved draft's release left the sent text as the box's own saved text, so the relaunched box showed it beside the host's row. A structured send now keeps its message in the saved draft's `heldSends` list, under the id the host knows it by, saved in the same write as the emptied box; the live text and other fields are untouched, so typing and later sends never replace it and no write waits. The host holding the message (the agent accepted it, it keeps it as a card, or a submission handed that card off) removes it, and a refusal's new id renames it. After a relaunch the box shows at once; once the chat's journal answers, each held send the host holds is dropped and the rest go back into the box. One predicate decides "holds" for the send's reply, the journal and the relaunch. The editor's rich-text cache moves to its own module so the draft cache stays within its size limit.
… in the comments that still described the old save
…fts-survive-restart
Contributor
Author
|
Closing as superseded by #24905, which landed the same behaviour: an unsent chat draft survives a reload or quit. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
A half-typed message in a chat's message box now survives quitting and reopening Orca, including its images. Before this, quitting Orca threw away whatever you had typed but not sent. A chat also has one message box everywhere it is open: if the same chat shows in two places, typing in one shows in the other.
This PR is step 1 of: drafts (this) → launcher on agent.launch → outbox removal.
Related: a separate host change will keep accepted messages across quit and restart (PR TBD). Until it lands, this PR keeps a sent message in the saved draft until the agent has accepted it, because today's host can still lose a message before that: it rejects one it has not handed to the agent yet when Orca quits or after a crash, and after a crash it settles one it handed over, but the agent never recorded, as never delivered (see Known limitations).
What Changed
Before:
After:
/model,/compact, ...; cleared once the host accepts them), and a power loss in the last seconds. A message whose delivery was not yet settled can come back into the box although the host had it (see the first Intended entry under Differences).Which chat a draft belongs to:
/clearand on resume. Its agent lives in exactly one pane, so that pane (its tab and split position, both saved across restarts) identifies the chat.How the saving works:
<userData>/native-chat-drafts/(the file is named by a hash of the chat's key, so a pane key's:is safe on Windows). A write goes to a temp file that is renamed over the old one; a clear deletes the file. On Windows both the rename and the delete retry briefly when another process (such as a virus scanner) holds the file open. A write that still fails is retried by the chat's next write or, failing that, once when Orca quits; meanwhile a reloaded window is never shown the text a failed clear left in the file. At load, Orca deletes only files it can prove are broken; a newer build's draft, or a file it could not read this time, is kept. Writes for one chat are applied in the order they were asked for, so a typing save still in flight can never land after the clear behind it. The main process confirms each write once the file operation returned, and waits for any still in flight when Orca quits. Only the app window may read or write drafts.orca:nativeChatComposerDraft:v1:<chat key>), never on the runtime server it is paired with.pendingis not enough, before or after the hand-over: the host answerspendingas soon as it records a message, but rejects every message it has not handed over yet when Orca quits, and after a crash when the chat next opens; and a message sent while the agent works is handed into the running turn within milliseconds, yet if the agent never recorded it before a crash, the reopened chat settles it as never delivered and the transcript no longer shows it. So the draft keeps apendingmessage until the agent accepts it. A message the host recorded and that fails before that (for example, the agent failed to start) also keeps its saved copy. Today its row offers Retry; once the separate transcript change (fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat #24710) lands, such a message shows as a "Not sent" row with no Retry, and its images cannot be resent from it, so the saved copy is how it gets sent again. Closing the window does not end a held send. A message that was not delivered stays held: one refused before it reached the outbox comes back into the box instead; one the host refused before recording it stays held while it waits for Retry (under its new id, if the refusal gave it one) until it is delivered or withdrawn; one the host recorded and rejected stays held the same way, and its row offers Retry today but, after fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat #24710, reads "Not sent" with no Retry; and one dropped with its delivery unconfirmed stays held. For a chat over a terminal agent (typed, or picked from the slash-command menu), the box's clear is saved once the terminal write ran, as the box is then, so anything typed after Enter is kept (typing may also be saved earlier, on its usual 300 ms pause, replacing the old text); a terminal write the agent rejected keeps the message saved. There, each send on a chat is numbered, and a send's save is skipped only while a later send on that chat still waits. A chat that ends forgets its sends and their held copies. Host commands (/model,/compact,/goal, ...) still clear only once the host accepts them. The web client instead saves the clear at Enter, before the outbox takes the message: browser storage reaches disk on the browser's own schedule anyway, so waiting would protect nothing, and clearing first frees the storage quota the outbox shares with drafts, so a large draft cannot make the send fail. A crash of the whole browser can still bring sent text back there.'persisted': written to the file on desktop, to browser storage on the web; or'memory-only'). The step-3 importer waits for'persisted'before deleting an old saved message. A saved draft can also record which old saved messages it already took in (importedOutboxEntryIds), so a crash between the two steps cannot import one twice; this PR reads and keeps that field, and the step-3 importer writes it. If saving fails or is unavailable, the draft stays in memory, and typing and sending keep working.<userData>/native-chat-attachments, one folder each, removed 30 days after they were made (checked 30 s after start, then hourly). Orca creates that folder owner-only; pastes and drops both accept an existing one that is a real directory, not a symlink. Terminal paste and terminal drops are unchanged. The folder is an allowed root for Orca's file access checks, but it is not a workspace root, so search and the explorer don't see it.How the code changed:
native-chat-draft-cache.tsis the store: one entry per chat (text, an ordered list of image chips, and the terminal input-line seed). Views subscribe to it and re-read it right after subscribing. Chips are added, removed, settled and cleared by id; a chip still being saved is in the list but never written to disk. Clipboard previews stay with the pane that pasted, and are released when the chip leaves or the pane unmounts.native-chat-draft-storage.tsis the saving layer: it batches typing and writes clears and put-backs at once. A structured send's message is kept as a held send in the saved draft:clearNativeChatDraftForHeldSend(cache) empties the box without saving it, andnative-chat-held-sends.tssaves the emptied box with the held send, removes it once the host holds the message, renames it when a refusal gives the message a new id, and after a relaunch drops or puts back each held send the earlier run left (tracked innative-chat-restored-held-sends.ts). Whether the host holds a message is one rule,structuredAgentSessionHostHoldsMessageinstructured-agent-session-message-delivery.ts, read from the send's reply (the outbox dispatch), from the journal (use-native-chat-held-sends.ts, in the session hook) and at relaunch; the same module watches each send's outbox entry for a withdrawal, a drop or a new id. A send to a terminal agent is held byclearNativeChatDraftForSend, with the per-chat send numbers innative-chat-draft-send-holds.ts. It saves throughwindow.api.nativeChat.drafts: on desktop that is IPC to the new main-processnative-chat-draft-store.ts(built on the existing per-file sidecar helpers, which gained a delete and a no-fsync write mode); in the web client it isweb-native-chat-drafts.ts, over browser storage, which also follows other browser tabs' writes.src/shared/native-chat-draft-record.ts, shared by both stores, so a damaged file or storage entry is dropped rather than trusted.native-chat-attachment-store.tsowns the image folder and its sweep.appendNativeChatDraftNow.While a pane is in the middle of an IME composition (typing Korean, Chinese or Japanese, for example), the composition owns that pane's box, as it already did for every other change Orca makes to the box. Text another pane writes during the composition waits, and the settled composition is written over it. The composing pane's own keystrokes reach the chat only when the composition settles, so a box that still holds text another pane just sent can never write it back. A clear still reaches the composing pane at once, which keeps only the newly composed characters.
Not affected: nothing new goes over the wire. The new IPC channels run between the desktop app's own window and its main process, and the web client saves locally, so remote and SSH hosts and mixed client and host versions see no change. Folder workspaces and git worktrees behave the same. The phone app is unchanged. An SSH chat's pasted image is still saved in the remote host's temp folder.
Why
This is the first step in removing the desktop's saved list of unsent structured-chat messages (the "outbox"). That list has caused a series of bugs, for example #24232, #17383, #20659 and #23026. Once it is gone, a message the host refuses goes back into the message box. Without saved drafts, that message would then be lost if you quit Orca before resending it. Saving the draft first makes the removal safe.
The draft belongs to the chat, not to the pane, because the message is the chat's: two panes on one chat should not hold two different unsent messages, and the draft should follow the chat wherever it opens.
Alternatives considered:
Differences from the common pattern
pending./model,/compact,/goal, ...) clear the box only after the host accepts them, where the common pattern clears at Enter and restores on failure. Plain messages already clear before they are handed over. Follow-up: step 3 of the outbox removal moves every clear to Enter.Known limitations
structured-agent-session-host-teardown.tsabandons it as "chat closed" throughstructured-agent-session-host-lifetime.ts) and, after a crash, when the chat next opens (structured-agent-session-delivery-loop.ts, "host restarted"). Live QA lost such a message on an earlier version of this PR, which released the draft on the host's firstpendingreply. A message the host handed into a running turn can also be lost: if the agent never recorded it before a crash, the reopened chat settles it as never delivered (journal-submission-reconciler.ts), and the transcript hides such a message unless the desktop's outbox still holds it, which a crash can lose (its browser storage reaches disk seconds later). This PR therefore keeps the draft until the agent accepts the message, so it comes back into the box after the relaunch instead of being lost; it may also show as a row for that message, as on main: a Retry or Resume row today; once fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat #24710 lands, a rejected one reads "Not sent" with no Retry. Messages the host queued as cards are kept across quit and restart by the host itself. The separate host change will remove both losses.Linked Issue
Part of the structured-chat outbox removal plan (step 1 of 3).
Visual Proof
Live app on a second Mac (macOS), driven over the Chrome DevTools Protocol with the app hidden. Head and SHA are stated per image.
Unsent text and a pasted image, before a normal quit. (d149ce0)

After relaunch, the same text and image are back, with a real thumbnail. (d149ce0)

App killed ~120 ms after Send, before the agent accepted the message: after relaunch the text is back in the input box and was not sent. (d149ce0)

App killed 300 ms after Send, after the agent accepted it: the message and reply are in the conversation, and the input box is empty. (d149ce0)

Send, then type "after enter" right away, then the app is killed 1 s later: the sent message went out, and "after enter" is back in the input box. (d149ce0)

App killed with unsent text and no Send: the text is restored after relaunch. (d149ce0)

App killed ~1 s after sending a follow-up while the agent was working: the follow-up never reached the agent, and its text is back in the input box. The conversation also shows it as a message bubble. (d149ce0)

KNOWN TEMPORARY COST (not a failure): a follow-up sent while the agent was working, then a normal quit 1 s later. After relaunch Orca offers to resume the chat, the follow-up shows "Message delivery is unconfirmed. Retry", AND its text is back in the input box. The message is kept, never lost. (d149ce0)

Testing
native-chat-draft-store.test.ts(main, real files in a temp folder): a child process saves a draft, clears it, waits for the confirmation and kills itself with SIGKILL; a fresh store finds no draft, also with a typing save still in flight when the clear was asked for. Without the confirmation waiting for the delete, or without per-chat ordering, it goes red. Also: drafts load oldest first with their images, seed andimportedOutboxEntryIds; writes for one chat apply in order; a clear deletes the file; damaged files (not JSON, or not a valid draft) and a killed write's temp files are removed, and a write made while loading wins; only the newest 128 are kept; a failed write reportsfailedwithout throwing.native-chat-drafts.test.ts(main IPC): the app window saves, loads (async and sync) and clears; any other renderer is refused for reads and writes; a write still in flight at quit is waited for; a reopened window (macOS keeps Orca running with none) gets the same store, so a failed clear is still not shown and is still retried at quit.desktop-startup-ordering.test.ts: the quit barrier waits for draft writes.sidecar-snapshot-file.test.ts: existing sidecar writers still flush to disk by default; only an explicit'process'write skips it; a delete treats a missing file as removed.native-chat-draft-store.bench.test.ts(runs only withORCA_NATIVE_CHAT_DRAFT_BENCH=1): 1,000 iterations each of a clear alone, a clear after a typing save, a clear with a typing save in flight, and a put-back rewrite, against a budget of p50 ≤ 2 ms and p99 ≤ 20 ms.native-chat-draft-startup-load.test.tsx: a chat's first render shows its saved draft from the startup load; it reads synchronously when it mounts first or the load failed; startup loads drafts before the session hydrates.web-native-chat-drafts.test.ts: the web client saves, loads and clears in browser storage only, never through the runtime, reports a refused write, and hears another tab's changes.native-chat-draft-persistence.test.ts(relaunch by reloading the module over the same storage, through the web client's store): text and images come back; typing waits 300 ms;pagehideand hidden-window flushes; a clear, and a clear with a held put-back, are on disk at once (outside a send, whose clear is covered below); put-back reportspersisted; restore-if-empty; full or missing storage keeps the draft in memory; drafts of a closed session, pane or terminal tab are deleted from memory and disk; the cap never drops a draft a view is showing; another web tab's save or send is followed, but not over this tab's unsaved typing; a write between a view's render and its subscription still shows; a closed tab's input-line seeds are forgotten.native-chat-structured-send-composition-clear.test.tsx, through real composers: Enter saves the emptied box and the sent message, under its id, in one write; the held send stays until the host holds it, and only that send's copy goes; closing the window (pagehide,beforeunload) keeps both the held message and its outbox entry (live QA case 12); text typed after Enter is kept; a message refused before the outbox comes back and its saved copy is never cleared; one the host refused under a new id stays held under that id until it is delivered; a later send the host holds goes while an earlier one waits for Retry, and an earlier one goes while a later one still waits; the web client's clear is saved at Enter, before the outbox takes the message; a composition the clear landed in never saves the sent text back, even with a put-back held; with two views of one chat, the sending view never shows sent text again while the other composes; a restored image whose file is gone blocks Send until removed, with the reason shown under the images until then.structured-agent-session-outbox-dispatch.delivery.test.ts: an accepted reply and a queued (card) reply release the saved draft; apendingreply keeps it until the agent accepts the message, whether not handed over yet, handed into the running turn, or from an older host; a rejection, a refusal (followed to its new id) and a send dropped unconfirmed keep it.use-structured-agent-session-outbox.host-has-message.test.tsx: the journal showing the message accepted releases it; showing it accepted by the host but not handed over, or handed over but not accepted by the agent, keeps it. The composer test anduse-native-chat-pty-composer-send.test.tsxalso run a send's whole clear (text, image chips, editor, and for a terminal chat the launch input-line seed) and check that nothing without the message is written before the host has it.native-chat-held-sends.test.tsx(relaunch over the same storage): a held send the earlier run left is dropped when the journal shows the agent accepted it, a card holding it, or a submission that handed that card off; it goes back into the box after the typed text when the journal has no submission for it, has it only pending, or cannot be read; the box shows and saves at once, and a send made while the journal is read is kept; in a running session, the journal releasing a held send once it is accepted, not while pending; other saved fields (importedOutboxEntryIds) survive.native-chat-draft-record.test.tsandnative-chat-draft-store.test.ts: held sends are parsed, ignore fields a newer build adds, and survive the main store.native-chat-draft-send-holds.test.ts: an earlier send's save waits while a later one is in flight; closing the window ends no hold; an ended chat's sends are forgotten (also throughdiscardNativeChatDrafts, innative-chat-draft-persistence.test.ts).use-native-chat-composer-attachments.test.tsx: only a chip whose file is proven gone is marked (an unreachable host, a runtime server's file, or a chip with no recorded location is not), each restored chip checked once; a restored local image is granted for reading before it is checked, and its preview waits; every view and the saved draft keep one chip order while a paste saves and chips are added and removed; a pending chip shows in every view but never reaches disk; a pane's previews are released when it unmounts; two views attaching in the same millisecond keep both chips.use-native-chat-pty-composer-send.test.tsx(chats over a terminal agent): the saved draft keeps the message until the terminal write ran, then holds the empty box; text typed after Enter survives; a message still waiting for its typing pause at Enter is saved first; an earlier send's save waits while a later one is in flight; a write the agent rejected keeps the message saved.use-native-chat-picker-command-dispatch.test.tsx: a command picked from the menu is saved the same way.web-native-chat-drafts.test.tsalso: a browser with no storage reports it as unavailable, which is not logged.native-chat-draft-send-kill.test.ts(main, real files): the composer's draft cache saving through the main store in a child process that kills itself with SIGKILL mid-send. Killed before the host has the message, the draft is on disk; killed after the host had it and the save landed, there is none.native-chat-draft-store.failed-write.test.ts: a clear whose delete failed is not served to a reloaded window, is retried at quit, is superseded by the chat's next write, is dropped (and logged) when the retry fails too, and never outlives a newer write; the drafts folder is created once per run.native-chat-draft-store.test.tsalso: a newer build's draft and a file that could not be read are kept, while JSON with no newer version ({},[], an older version) is removed; load never deletes a draft written while it ran.sidecar-snapshot-file.test.ts,fs-utils-remove-retry.test.ts,durable-file-write-process.test.ts: a delete retries a Windows sharing violation; stale temp files are swept once per file per run; a renamed temp file is not removed again.native-chat-draft-persistence.test.tsalso: typing is flushed onbeforeunload; a web tab sees another tab's send made before its chat first read drafts; a write a take-back waited for that failed or timed out is logged once with its chat and reason, never its text.use-structured-agent-session-queued-messages.test.tsx: a queued message taken back is deleted from the host only once its copy is written, and still deleted when that write fails or stalls.use-native-chat-launch-draft-adoption.test.tsx: a structured chat's launch prompt survives a relaunch; an edited multi-line terminal seed is seeded again after a relaunch, so the send replaces the whole input line.use-native-chat-composer-paste.test.tsx: a local paste asks for chat storage, an SSH paste does not.NativeChatImageAttachmentPreview.test.tsx: a restored image is read only once its check is done.structured-agent-session-tab-retirement.test.ts,terminal-tab-close-map-identity.test.ts(tab close, and clearing a launch draft forgets its seed),pending-split-close.test.ts,bulk-worktree-purge-terminal-maps-leak.test.ts,native-chat-launch-draft-teardown.test.ts(deleting a worktree deletes its terminal chats' drafts and seeds),purge-stale-runtime-host-state.test.ts(a removed runtime keeps its chats' drafts).native-chat-composer-workspace-file-drop.test.tsx: an image dropped from a runtime workspace records that it lives on the runtime server.structured-agent-session-withdrawn-message-restore.test.tsx: an image a Stop gives back is checked like a local one in a local chat, and keeps its SSH connection in an SSH chat.native-chat-draft-persistence.test.tsalso loads chips saved without a location.native-chat-attachment-store.test.ts(one folder per image under user data, created owner-only, readable with no grant from this session, a non-private folder still works but a symlinked one is refused, a composer drop copies into the folder after a paste created it, 30-day sweep of pastes and drag copies),clipboard-image-temp-file.test.ts,native-file-drop-relay.test.ts(composer drops copy into chat storage under its folder rule, terminal drops keep the private temp root).Send and save latency (this Mac, Apple silicon):
Main-process store, 1,000 iterations each: clear alone p50 0.03 ms / p99 0.15 ms; clear after a typing save 0.08 / 0.26 ms; clear with a typing save in flight 0.33 / 0.95 ms; put-back rewrite 0.28 / 0.59 ms. Re-run three times after the review fixes on a shared, busy machine: p50 0.03-0.49 ms and p99 0.1-5.8 ms across the four cases, all inside the 2 ms / 20 ms budget.
Enter to terminal write, 300 sends through the real store (without Electron's IPC hop): 0.016-0.019 ms p50 and 0.06-0.09 ms p99, the same as without saved drafts (0.01 ms / 0.06-0.07 ms); nothing on the send path waits for a save any more. The main-process store after this change: p50 0.03-0.22 ms and p99 0.15-1.7 ms across the four cases.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)