Skip to content

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
mainfrom
brennanb2025/chat-drafts-survive-restart
Closed

brennanb2025 wants to merge 67 commits into
mainfrom
brennanb2025/chat-drafts-survive-restart

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 44 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3882 $\color{#cf222e}{\Huge{\mathbf{−}}}$​124 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3758
Prod 71 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2614 $\color{#cf222e}{\Huge{\mathbf{−}}}$​422 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2192

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:

  • The text and images you had typed into a chat's message box lived only in memory. They survived switching a pane between chat and terminal view, but quitting Orca, a crash, or a window reload lost them.
  • Pasted images were saved to the OS temp folder, which a reboot or the OS cleanup can empty.
  • Each pane kept its own draft, even when two panes showed the same chat.

After:

  • Each chat's unsent text and attached images are saved and come back when Orca starts again. This covers structured chats and chats shown over a terminal agent.
  • In the desktop app, a crash or quit right after Enter does not lose the message, and once the host holds it for good (the agent accepted it, or the host queued it as a card) it does not come back as a draft. Until then the saved draft keeps the message beside the box, so a crash or quit puts it back. Exceptions: a crash in the first few milliseconds after Enter, before the write that saves the emptied box with the message has landed (the box then comes back with the text as it was before Enter, and shows it twice only if the host recorded the message even sooner; see "What a crash or quit can still repeat"), a save that failed, host commands (/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).
  • A chat has one draft. Every view of it reads and writes that draft live: a clear at send, or text put back by Stop, shows in all of them, and they all show the same images in the same order, including one still being saved.
  • Images you paste or drop (macOS screenshot drags) into a chat are kept in Orca's own storage, so they survive a reboot and their thumbnails load after a relaunch. An image attached from elsewhere on disk gets its read permission back after a relaunch, so its thumbnail loads too. If a restored image's file is gone, its chip is marked, a note under the images says "Image no longer available. Remove it to send.", and Send waits until you remove it.
  • A launch prompt that arrives as an unsent draft (for example a linked issue, or a fork into a new worktree) is saved like any draft.
  • A draft is deleted when its chat ends. Removing or re-pairing a remote runtime does not delete drafts: its chats live on the host and come back with it.
  • In the web client, a second browser tab follows what another tab saves or sends, instead of showing (and saving again) text already sent.

Which chat a draft belongs to:

  • A structured chat is identified by its session id, so its draft follows it to any pane that shows it. Closing its tab ends the session, and the draft is deleted with it.
  • A chat over a terminal agent has no stable session id: it changes on /clear and 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:

  • In the desktop app, the main process keeps the drafts: one small file per chat under <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.
  • On desktop, startup loads the drafts alongside the session read, before any chat mounts, so a chat shows its draft on its first paint. If that load fails, the first chat to mount reads them synchronously instead; a failure never blocks startup. The web client skips that step and reads browser storage when a chat first needs a draft, which is also when it starts following other tabs, so no other tab's change is missed in between.
  • The web client keeps its drafts in its own browser storage, one key per chat (orca:nativeChatComposerDraft:v1:<chat key>), never on the runtime server it is paired with.
  • Typing is saved after a 300 ms pause, and any pending save is written at once when the window is hidden, the page unloads or the page closes. Every write says which it is: a put-back, or a clear outside a send, is always written at once, whatever text remains.
  • Sending: Enter empties the box and sends at once; nothing waits for a save. For a structured chat, the saved draft keeps each sent message beside the box, as a held send under the id the host knows it by, from Enter until the host holds it: the emptied box and the held send are saved in one write as soon as the send has its id. Typing after Enter, and later sends, change only the box, so they are saved as usual and never replace a held message; a refusal that gives the message a new id renames its held send. The host has it for good once the send's reply or the journal shows the agent accepted it, or the host queued it as a card, whichever comes first; a message withdrawn back into the box, or dropped, settles too. pending is not enough, before or after the hand-over: the host answers pending as 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 a pending message 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.
  • What a crash or quit can still repeat: the write that saves the emptied box with the held send is issued as soon as the send has its id, about 2 ms after Enter, while the host recorded the message about 46 ms after Enter in live traces. A crash after the host recorded it but before that write landed (a write slower than the host) brings back the text as it was before Enter, beside the host's own row. Step 3 of the outbox removal closes this gap; this PR does not make the outbox wait for the write. After a relaunch the box shows its live text at once, and saving works as usual. The chat's held sends wait for its journal: each one the host holds (today: the agent accepted it, or the host keeps it as a card or handed that card off) is dropped, and the others go back into the box after anything typed meanwhile, also when the journal cannot be read. Nothing is shown and then taken back. So a crash between the host holding a message and the release of its held send no longer shows it twice, with two exceptions (Known limitations): the relaunch reads only the chat's newest history page, so a held send whose host record is older than that page is put back although the host has it; and if a remote chat's first history read fails before its connection is up, held sends are put back. Both can show the message twice, never lose it. Before the host holds it, the held send keeps the message, so a relaunch can put it in the box while the host also has it. A message sent while the agent works is accepted only when the running turn takes it in, which can be the end of that turn: a normal quit or crash before then brings the message back into the box, next to its outbox row (on today's host, the "Resume" row), and Enter would send it twice. The same holds when the host's reply was lost while a remote host was unreachable. Until step 3 a structured message the agent has not accepted is also in the desktop's outbox; see the Temporary entry under Differences.
  • Text put back into the box (a message withdrawn by Stop, a queued message taken back for editing) is written at once. A queued message taken back is deleted from the host only once its copy in the box is written (waiting at most 250 ms, then deleting anyway). Images a Stop gives back take the location of the chat's owner, as new images in that chat do.
  • Writes that put text back report whether they were saved ('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.
  • A draft is deleted with its chat: when its structured chat tab closes, when its terminal pane or tab closes, when its worktree is deleted, and when a purge removes the worktree (repo removal, folder workspace removal, a listing that shows it gone). A cap of 128 saved drafts remains only as a backstop, and it never drops a draft that a view is showing. Damaged entries (not JSON, or not a valid draft), any past the cap, and temp files left by a write that was killed are removed when the drafts first load.
  • A chat over a terminal agent also saves the launch text Orca typed into the agent's input line, because the line keeps it across a relaunch. After a relaunch the send still replaces the line instead of pasting the draft on top of it. It is forgotten once the launch text is sent, resolved, or its tab closes.
  • Pasted and dropped chat images go to <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.
  • Each image chip records where its file lives: this machine, an SSH host, or a remote runtime server. A restored image is checked once per view when this machine can ask about it: a local one is first granted for reading again, as attaching does, and one on an SSH host is asked through that connection. A file proven gone is marked missing. A file on a runtime server, a chip saved without a location, or a check that cannot answer (an unreachable SSH host, a refused grant) leaves the chip as it is.

How the code changed:

  • native-chat-draft-cache.ts is 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.
  • A new native-chat-draft-storage.ts is 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, and native-chat-held-sends.ts saves 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 in native-chat-restored-held-sends.ts). Whether the host holds a message is one rule, structuredAgentSessionHostHoldsMessage in structured-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 by clearNativeChatDraftForSend, with the per-chat send numbers in native-chat-draft-send-holds.ts. It saves through window.api.nativeChat.drafts: on desktop that is IPC to the new main-process native-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 is web-native-chat-drafts.ts, over browser storage, which also follows other browser tabs' writes.
  • The draft record's type and its parser live in src/shared/native-chat-draft-record.ts, shared by both stores, so a damaged file or storage entry is dropped rather than trusted.
  • A new main-process native-chat-attachment-store.ts owns the image folder and its sweep.
  • The pane key now only identifies the pane's editor (its rich-text state) and its file-drop target.
  • The two "put back" calls (text, then images) became one call, 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:

  • Saving every write on a delay, the simpler shape. Rejected because a delayed clear lets a crash right after Enter bring sent text back and send it twice.
  • Keeping desktop drafts in the browser's local storage, writing the clear at once. Rejected after live QA: the browser commits local storage to disk on its own delayed schedule, and gives no way to wait for that commit. Killing Orca 0.3, 1 or 3 seconds after Enter brought the sent text back 9 times out of 9. Asking Electron to flush storage does not help: the call has no completion, so the send still cannot wait for it.
  • Deciding on relaunch, from the chat's history, whether a restored draft was already sent. Built and then removed: it matched content, so it had to be extended for every send path (queued messages, the first message of a chat, host commands, each terminal agent's transcript), and an edited-then-sent draft could still come back. Saving the clear in order with the send removes the case instead.
  • Saving the clear, and waiting for it, before handing the message over (an earlier version of this PR). Rejected after live QA: killing Orca 26 to 64 ms after Enter lost the message 6 times out of 6, because the clear was on disk before the host had the message and the outbox entry was still only in browser storage. It also added a wait to every send.
  • Keeping drafts on the host and clearing them when it accepts a message. Rejected: it needs a new remote call with version negotiation, typing would depend on the network, drafts would be unavailable while a host is unreachable, and chats over a terminal agent have no host record.
  • Writing the draft files with a full disk flush (fsync). Rejected for the send path: it would put a full flush of the drive's cache on every send, and a typing save on the same chat would hold the clear behind its flush.
  • One storage key for all drafts. Rejected because every keystroke would rewrite every chat's draft; one key per chat writes only the draft that changed.
  • Keeping image bytes in the saved draft. Rejected because pasted images can be many megabytes, and the draft shares its storage with the outbox; agents read images by path, so a file in Orca's own folder is enough.
  • Letting another pane's text replace a box mid-composition. Rejected because Orca has already shipped this bug: writing into the box during a composition lost and mangled syllables ([Bug]: Hangul/CJK composition is overwritten in the Native Chat composer while a turn streams #16911). The one change that does reach a composing box is a clear, because dropping it resurrected sent text ([Bug]: Native Chat structured send can resurrect a just-sent message into the next one #17359).

Differences from the common pattern

  • Intended: desktop drafts are kept by the main process in one file per chat, and a sent message is kept in the saved draft, beside the box, until the host has it; after a relaunch, the box shows its live text at once and a held message appears in it once the chat's history has loaded (or failed to), unless the host holds it. In the common pattern, drafts live in the browser's local storage and the clear is saved at send. In Orca that shape brought sent text back after a crash: killing Orca 0.3, 1 or 3 seconds after Enter restored the sent message 9 times out of 9 in live QA, because local storage reaches disk seconds to minutes later and cannot be waited for. Saving the clear before the host had the message instead lost it 6 times out of 6 (kills 26 to 64 ms after Enter). Which chat a draft belongs to, when it is cleared and restored, and when it dies are unchanged; where it is saved, and how long a sent message stays saved, differ. The web client keeps browser storage. The files are written without a full disk flush, the same protection local storage gives: they survive Orca being killed, but a power loss in the last seconds can lose the newest change. A consequence of keeping the draft until the agent has accepted the message: if Orca quits or crashes before that is known (the agent was busy and the message waits for the running turn to take it in, or the host's reply was lost while a remote host was unreachable), the relaunched box can hold a message the host had, so Enter would send it twice. This has no fixed bound; for a message sent mid-turn it can last until that turn ends. Today's host can lose a message the agent has not accepted (Known limitations), so keeping it is what prevents a loss. The common pattern also puts a message back into the box on any send error, timeouts included; what differs is that Orca's copy survives a crash.
  • Temporary: on desktop, until step 3, a structured message the agent has not accepted yet (still sending, handed into a running turn that has not taken it in, waiting behind a busy agent, or held for Retry after a refusal or failure) is in two places: the saved draft (so a crash or quit restores it) and the desktop's outbox. If Orca quits or crashes while both hold it, the relaunched chat can show the message in the box and also as the outbox's row, or send it again from the outbox. Once the agent has accepted it, the draft no longer holds it. Follow-up: step 3 of the outbox removal deletes the outbox; the host change above stops the host losing such messages, after which the draft can stop at the host's pending.
  • Intended: images a draft holds are removed 30 days after they were saved; past that, the chip shows as missing and blocks Send until removed. In the common pattern, app-owned upload files that nothing points to are swept after a grace period, and a missing one is refused when you send. Orca uses the same folder-and-sweep shape with a longer window, so drafts can wait weeks.
  • Temporary: images already sent also expire after 30 days, where the common pattern keeps a file as long as its conversation exists. Follow-up: keep a chat's images while the chat exists and delete them with the chat.
  • Intended: while a pane is mid-composition, another pane's text waits until the composition settles, and the pane's own keystrokes reach the chat only when it settles. Writing into a composing box lost and mangled syllables in Orca ([Bug]: Hangul/CJK composition is overwritten in the Native Chat composer while a turn streams #16911).
  • Intended: a cap of 128 saved drafts remains as a backstop; the common pattern has no bound. Drafts die with their chat, as in the common pattern, but Orca has chats whose end this client never sees: a remote session closed from another device, or a runtime removed while it was unreachable. Losing contact with a host is never proof its chats ended, so Orca cannot discard those drafts itself, and without a bound they would stay on disk forever. The cap keeps the newest 128 and never drops a draft a view is showing; the in-memory draft cache was already bounded the same way (perf(renderer): LRU-bound the native-chat composer scope caches #7566).
  • Temporary: host commands (/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.
  • Temporary: a restored image is checked when its draft is restored, not again at send. Follow-up: check at send once sends become host round trips (step 3).
  • Temporary: a skill picked from the menu is saved as its plain text token, not as a chip, so after a restart, or in a second view of the same chat, it shows as text. Follow-up: derive skill chips from the text.

Known limitations

  • After a relaunch, held sends are checked against the chat's first history page only (its newest items, up to 200). A held send whose record on the host is older than that page, which needs the host to keep working on a remote or SSH runtime while Orca is closed, is put back into the box although the host has it: a possible duplicate, never a loss. Step 3 replaces this with a host query by message id. Not verified live: when a remote chat's first history read fails because its connection is not up yet, the held sends are put back the same way.
  • Today's host rejects an accepted message it has not handed to the agent yet when Orca quits (structured-agent-session-host-teardown.ts abandons it as "chat closed" through structured-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 first pending reply. 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)
pr24461-d149ce0-i1-before-quit.jpg

After relaunch, the same text and image are back, with a real thumbnail. (d149ce0)
pr24461-d149ce0-i1-after-relaunch.jpg

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)
pr24461-d149ce0-i3-100-1.jpg

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)
pr24461-d149ce0-i3-300-1.jpg

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)
pr24461-d149ce0-iae-1.jpg

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

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)
pr24461-d149ce0-i12k-1.jpg

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)
pr24461-d149ce0-i12-1.jpg

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 and importedOutboxEntryIds; 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 reports failed without 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 with ORCA_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; pagehide and 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 reports persisted; 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; a pending reply 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 and use-native-chat-pty-composer-send.test.tsx also 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.ts and native-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 through discardNativeChatDrafts, in native-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.ts also: 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.ts also: 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.ts also: typing is flushed on beforeunload; 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.
  • Lifecycle: 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.ts also loads chips saved without a location.
  • Main: 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).
  • Each rule was removed in turn, and a test went red every time.
  • Typecheck (node, web and CLI projects), lint, and the localization checks pass locally. Live app QA: see Visual Proof.

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

  • Security: desktop drafts are files under the app's user data folder; the folder is created owner-only and each file is written owner-only. Only the app window may read or write them over IPC. The web client keeps them in its own browser storage, where the outbox already keeps message text. The image folder is an authorized root for Orca's file access checks for the whole session (every chat's images, not one file at a time), and it holds only images the user pasted or dropped into a chat.
  • Performance: one small file write per chat after a typing pause, with no full disk flush. A send waits for no save (numbers under Testing). Startup reads the saved draft files (normally at most 128), alongside the session read. The image sweep lists one folder hourly.
  • Backwards compatibility: no released build saved drafts, so nothing needs migrating. An older build ignores the drafts folder and the web client's storage keys.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
…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.
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.
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.
…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.
…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.
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.
…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.
…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.
…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.
@brennanb2025

brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status: not ready yet, waiting on two host changes

The 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).

  • A half-typed message, with its images, comes back after you quit or Orca crashes.
  • The box empties the moment you press Enter. A sent message is kept as a separate held copy until the host has it for good (the agent accepted it, or the host queued it as a card):
    • a crash before that puts the text back in the box;
    • after that, the box stays empty;
    • anything typed after Enter is kept on its own and is never replaced by the held copy.
  • On desktop the saved copy lives in a file per chat, written by the main process. Browser storage could not be made reliable here. The web client keeps browser storage.
  • A restored image whose file is gone shows "Image no longer available. Remove it to send." and blocks Send until removed.

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:

  • Sent text coming back after a crash. Fixed with the main-process store plus the ordering above.
  • Messages lost on a crash or quit right after Enter. The draft was released on the host's first "pending" reply. Today's host then rejects an accepted message it hasn't handed to the agent yet, at quit or after a crash. The draft is now held until the agent accepts the message.
  • Images.
    • Pasted images were saved to the system temp folder and could vanish. They now go to an Orca-owned folder, kept 30 days.
    • Restored previews were blank.
    • Missing images were sent silently.
    • Image order differed between panes.
  • Drafts never cleaned up. Drafts now die with their chat: tab, pane or worktree close. The 128 cap is only a backstop.
  • Typing in other alphabets. With two views of one chat, a second view mid-way through composing (for example Korean or Japanese) could write already-sent text back.
  • Failure cases: a failed clear on Windows, the web client's cross-tab sync, and queued take-back ordering.
  • Shown twice after a crash: a crash between the host saving a message and the draft letting it go showed it twice. Each sent message now has its own held copy, checked against the host by message id on relaunch, which narrows that to the first ~2 ms after Enter.

Not done yet (why this stays a draft).

  • Temporary: a message sent while the agent is working stays in the saved draft until the agent accepts it. After a quit in that window it comes back in the box and also as its Resume/Retry row, so Enter would send it twice. It is a duplicate, never a loss.
  • This PR relaxes back to "the host's ok means sent" once both host changes land:
  • It is then re-tested live and marked ready.
  • Deferred, P3:
    • on relaunch, held copies are checked against the newest page of chat history only (up to 200 items); an older one is put back, a possible duplicate, never a loss;
    • host commands (/model, /compact) still clear only after the host accepts;
    • a restored image is checked at restore, not again at send;
    • skill chips come back as plain text;
    • sent images also expire after 30 days.

Verified.

  • Live on a second Mac (a hidden test app, an isolated profile and home, a fake agent), at d149ce0:
    • nothing was lost in any case;
    • a kill about 130 ms after Enter restores the text, unsent;
    • a kill at 300 ms or later is sent, with an empty box;
    • a quit right after an idle send is sent;
    • text typed right after Enter survives a kill, and the sent message is not duplicated;
    • unsent text and images survive kills and quits;
    • the missing-image note works.
  • Pressing Enter costs the same as main within noise: median 29 ms vs 24 ms on the same chat (measured at 9a992ea).
  • Unit tests, each shown to fail with its fix removed. One kills a child process mid-send.
  • Typecheck, lint, and the host's journal state per run (read-only).

Not verified.

  • Terminal-agent chats live: there is no safe fake terminal agent.
  • Windows and Linux live.
  • A real reboot: the image file was deleted instead.
  • Cmd+Q itself: a graceful SIGTERM was used.

…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
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #24905, which landed the same behaviour: an unsent chat draft survives a reload or quit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant