* refactor(native-chat): remove the unused terminal handoff No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths. * fix(native-chat): never let the pre-stop snapshot hold a chat's stop Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop helpers only the terminal handoff called `claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and `queryWindowsProcessRowsFresh` lost their last caller with the handoff. The fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`, the teardown path that still depends on that contract. Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): stop citing the removed handoff in lifecycle comments Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stalled snapshot drain without a cast Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a start dead before proving owes no settlement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(terminal): remove the agent-session PTY write gate The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop the transcript helpers only the handoff called appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop calling a starting chat "mid-handoff" A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stand-in roster decoder without a cast Co-Authored-By: Claude <noreply@anthropic.com> * refactor(codex): name the pinned rollout lookup for what it does With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow. * refactor(native-chat): type the owner-status reply as the host sends it The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged. * refactor(native-chat): normalize terminal-handoff lease values once at decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind. * refactor(native-chat): stop threading the owner kind through a reservation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it. * test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write. * fix(native-chat): name a chat write by its target, not the owner generation A write carried the fence of the last frame the pane read, and the host refused it unless that fence was still current. An idle release and the restart after it each move the fence, and the release publishes nothing, so a send after a release was refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a cold start was refused as stale. Every write already names what it acts on: a send its conversation, a cancel its turn, a prompt answer its item revision, a rewind its epoch; an option is last-writer-wins. So admission stops comparing the client's fence, and the rebase that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it. The writer-lease check stays, and so does the attach's compare-and-swap. Frames now stamp the fence read when each frame is sent instead of a copy each subscriber kept, which went stale on the same release. * fix(native-chat): every journal append reaches the chats that are open A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted. * test(native-chat): an epoch replacement reaches the open chat * test(native-chat): each row reaches an open chat once, and a live handle enters only through the map * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. * perf(native-chat): a publish behind a delivered commit reads nothing Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read. * test(native-chat): state why the teardown test's fake journal is safe to cast * docs(native-chat): say mutation admission checks only the writer lease * docs(native-chat): drop the send rebase from comments that still described it * fix(native-chat): a message is accepted, then delivered A send to a chat with no running agent restarted the agent inside the send call, before the message was recorded, so the client waited for the whole start and a failed restart refused the message. Claude held prompts sent during startup, and those could settle as "unconfirmed". A send is now accepted inside the session's serialized queue: one ledger row and one submission row marked handoverRecorded, published, answered pending. A per-session delivery loop exists while a message is queued. It starts the agent through the same serialized attach a hold uses, waits outside the queue for a Claude child to prove its start, and hands the oldest queued message over as its own serialized step, writing dispatch{pending} before the adapter call. A start it needed and did not get writes one error-tone row and rejects every queued message with the same words; a start Stop cancelled writes none. Settlement follows from the rows. A queued message is provably unwritten, so a close, an eviction or an exit rejects it. A handed-over message stays in doubt. A queued row at or below the sequence a handle found when it opened was left by an earlier process and is rejected at open, with no latch. Stop withdraws queued messages with no writer lease and no fence. An attach failure keeps the conversation open, and the attach adopts its journal. Owed work counts the loop and queued rows. A compaction or rewind found prepared when a conversation opens was started under a child this process no longer has, so the open settles it rather than leaving it to refuse every send until a view attaches. The open cursor is scoped to its epoch, because sequences restart when an epoch is replaced. Deleted: restart-before-admission, recordFailedRestart, the fence rebase, Claude's startup gate, the attach's forget on failure and its own crash boundary. Clients without agent-session.accepted-send.v1 get their reply held until the handover; the desktop and paired desktop lists advertise it. * fix(native-chat): settle queued messages only for the child that ended A child that proved its start and then exited before its message was handed over left the message queued: the exit settlement returned early when nothing else was in flight. Delivery then started another child for it, and a child that died the same way started another, without end and without a row. A retried settlement for an earlier generation, run by the attach that delivery started, did the opposite: with that generation's turn unfinished it rejected the message queued for the child being attached. The settlement now takes the rejection for queued messages from its caller. The unexpected exit and the eviction pass one, and it applies even with no other work in flight; the retry for an earlier generation passes none. * fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. * perf(native-chat): the recovering open reads the journal once Every conversation open now goes through the recovering open, including the read restore of every chat at startup, which used to replay its journal once. The recovering open replayed it twice: once to probe it and again inside the open. The probe is now handed to the open as its load. * fix(native-chat): an attach that fails after indexing its child leaves no child behind A failed attach now keeps the conversation open, but a failure after `onAttached` indexed the child (the rewind or compaction recovery, or the attach's own success record) left that entry claiming a child the failure path had already released. The next send found the phantom, skipped the start, and wrote at a fence the journal had moved past, so the message stayed queued for good. The entry now drops the released child and its event sink, and follows the record's fence, as a failure before indexing already did. * fix(native-chat): a withdrawn message shows no error, and a rejection outlasts the send's answer The error strip for a message the host accepted and then did not deliver matched the entry before the outbox reconciled, so a Stop's withdrawal, which the reconcile drops, showed "Orca could not send your message" with nothing to retry. It now reads the reconciled entry. A rejection the journal records before the send's own pending answer lands is final as well: that answer no longer puts the entry back to dispatching with no Retry. * fix(orchestration): a structured worker whose agent outlasts the preamble wait is left unknown, not torn down The preamble waits for its submission to be delivered while the worker's agent starts. When that wait ran out it threw operation_unknown, and the failed-start teardown then closed the session, which rejected the very preamble the host was about to deliver. It now reports a turn start nobody observed yet: the worker is start-unknown with its session kept, the host delivers the preamble when the agent starts, and the worker's report settles the dispatch as for any unobserved start. The receipt no longer suggests reading a screen a structured worker lacks. * fix(native-chat): a message rejected while its chat was closed reads as not sent A remount reads an entry it left dispatching as unconfirmed. When the journal had rejected it meanwhile, as a failed start or a quit now does, the reconcile left it unconfirmed: it blocked every later message behind a Retry and no reason, and the delivery probe, seeing the journal already answered, never ran. The reconcile now settles it as rejected like a dispatching one. * test(orchestration): name why the readiness settlement fakes are cast * fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent * docs(native-chat): drop the fence from the admission the send effects run behind * docs(native-chat): give the fence move on release the reason that still holds * docs(native-chat): stop citing a write fence check in launch and mailbox comments Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease. * refactor(native-chat): the provider child is its own record A conversation now outlives any number of provider children, so the child is one record on the conversation's entry instead of five loose fields beside its journal. It is written in one place: indexed only once an attach has fully succeeded, and ended through one function that an exit, a failed re-attach, a Stop and an eviction all share, matched on the child's generation and fence. - A failed attach writes no child, so there is nothing to unwind: the field unwind and the fence patch after it are gone. - Conversation writes read the record's fence, the way mutation admission already does; a child's own writes use its fence. The four stored-fence patches, and the settlement retry's overwrite of the conversation's fence, are gone. - The owed wind-down is its own tombstone, carrying the child it is owed for, and is no longer dropped when an attach replaced the whole entry. - Stop on a child still proving its start stops only the child: its lease goes back and the chat is told it is idle, but the journal, the holders and the readers stay. Close is that stop plus the conversation's close. - The settlement retry uses the conversation's own journal, opened through the host's one open. * fix(native-chat): the delivery loop alone settles a message its start or child failed A queued message was settled by whichever path happened to end the child first: the loop, the unexpected exit, eviction's work settlement, the open's leftover rule, and the startup branch that rejected every pending row. That gave two failure rows with different tones for one start, a loop that could hand over to a different child than the one it waited on, and a Claude start that died while starting reading unlike every other failed start. - The loop remembers the child it waited on. At handover, if that child is gone or replaced, it reads how it ended: a Stop continues; anything else writes one failure row and rejects every queued message with the same words, then stops. A child still starting whose start the adapter says did not land fails the same way. The exit, eviction and the settlement retry only settle the handed-over and legacy rows of the child that ended. - One failure row, always an error, keyed by the start. A start a view began that dies with nothing queued writes the same row through the same builder, so a second report revises it. - The open no longer rejects leftovers; the loop's first step does, and the open wakes it. - `awaitStarted` answers why a start did not land, so the row says it even when the loop sees the failure before the exit is processed. - Quit closes every conversation the way closing a chat does: what is still queued is rejected as closed, with or without a child, and a start the loop already has in flight is waited for so the child it produces is stopped rather than left behind. * refactor(native-chat): a stopped child ends on the one reading of its stop The eviction step reads a stop's result through `stopAgentSessionProviderRoot` and hands that verdict to the child's ending, so the host never forms a second view of whether the root is gone. Every ending carries it: a stop's comes from that reading, an exit's root is gone by definition, and a failed re-attach passes what its release saw. The end-of-child record can therefore also carry a stop whose root was not seen to go, which nothing ends on yet. * feat(native-chat): the host says it accepts a send before any agent has it The host now lists agent-session.accepted-send.v1 among its own runtime capabilities, the same string capable clients already send. A client can then tell a host that answers a send at acceptance, and admits a Stop with no writer before a turn starts, from an older one that still restarts the agent inside the send. Additive: an older client ignores a capability it does not know. * refactor(native-chat): an attach never opens a journal of its own The attach adopts the conversation's open journal, which outlives it, so it no longer opens one for a direct caller either. That leaves nothing for a failed adopted import to close, and the flag that told the two cases apart is gone. Tests that attach without a host open the conversation the way a host does. * fix(native-chat): a moved fence resends nothing on a host that accepts first The outbox treated any fence change as a new owner: it dropped the answer of a send in flight, queued that send to go out again under the same id, and unblocked a refused head. On an older host that is how a send the restart refused, unrecorded, gets another try. On a host that records every send before it starts an agent, a fence moves because that start ran, so the same rule resent into every failed start. With a fence stamped on every frame, that became a loop. The outbox now reacts to a fence change only when the host has not advertised that it accepts a send before any agent has it. On such a host, only a Retry or a new send goes out, and a failed start reaches the client as a rejected message it keeps with its Retry. Against an older host, or before one has answered, the outbox behaves as it did. Desktop and paired web share this hook. * refactor(native-chat): a child's end says whether the user or the host stopped it The end-of-child record's cause now tells a user's Stop from the host stopping the child for a cause of its own: `user-stop` and `host-stop` replace `stop`. The delivery loop goes on after a user's Stop, as before, and fails the start it was waiting on after a host stop, with the one error row and every queued message rejected, in the stop's reason when it gave one. The reason stays description only. Stop passes `user-stop`; nothing passes `host-stop` yet. * fix(native-chat): a chat whose only work is a queued message is not offered for resume A message accepted while the agent was starting counts as working in the chat, and quit rejects it as never sent. The teardown snapshot read the same working rule, so a relaunch offered to resume a chat whose agent never had the message. The snapshot now reads only what was handed over. * test(native-chat): type the queued-message fixtures in the resume-offer tests * fix(native-chat): a start that dies while a message waits on it is that message's failed start Opening a chat's tab starts an agent for the view, and a send accepted meanwhile waits on it. When that start died, its exit wrote the start's error row and left the message queued, so the delivery loop started a second agent into the same failure and wrote a second row. A child's end now records where the conversation's journal stood, and the loop settles a message accepted before a failed start ended with that start: one row, under its key, and no second start. A message sent after the failure still gets a fresh start. * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): pin what a failed start settles, and what a resume offer names A view's child that dies while a sent message waits settles that message only when it died starting and no child has taken its place: a proven child's crash, or a second start since, gets the message delivered. The resume offer names the handed-over message, never a newer one still queued. * test(native-chat): the failed-start pins fail on what the message became, not on a timeout * test(orchestration): the preamble's host stub is typed, not cast The preamble send now takes only what it reads of the host, the send, the settlement wait and the record's fence, so its test builds that host with real types instead of `as never`. * fix(native-chat): a Stop that names no turn stops what the conversation has in flight Between handing a message to the agent and the agent opening its turn, there is no turn id a client could name, so a Stop in that gap was refused as "already finished" while the agent went on to answer. A cancel's turn id is now an optional precondition instead of its target: with none, the host withdraws what is queued and, when the journal still reads working, asks the adapter to stop whatever the child has in flight. Claude's interrupt is session-scoped, so it is guarded by fence and acquisition generation rather than a turn identity. Codex interrupts the turn its latest turn/start answered with until the journal shows one. A cancel that names its turn behaves exactly as before. * fix(native-chat): Stop is there from the moment a message is sent The composer showed Stop only once the agent had opened a turn, so for the second or two after a send the chat read "thinking" with no way to stop it. Against a host that takes a Stop naming no turn, Stop now shows whenever the chat reads working (a turn, a queued message, or a handed-over one still unanswered) or this client still has a message on its way. Pressing it, or Escape, first drops every outbox entry the journal does not hold yet, so nothing goes out after the Stop, then sends the conversation-wide cancel. A send already on its way reaches the host ahead of the cancel, which withdraws it there. Against an older host Stop still needs a running turn. The unconfirmed-send probe moves into its own hook so the outbox hook stays in budget. * fix(native-chat): Stop before a turn is gated on its own host capability A host that accepts sends first (agent-session.accepted-send.v1) can still predate the cancel that names no turn and would refuse it as invalid, since clients and hosts ship independently. Hosts that take that cancel now advertise agent-session.conversation-stop.v1, and the renderer shows Stop before a turn opens, and sends the no-turn cancel, only to a host advertising it. Every other host keeps a Stop that needs, and names, a running turn. The host capability probe the accepted-send hook used is generalized so both read one path. * test(native-chat): a build advertises conversation stop exactly where its cancel may name no turn * fix(native-chat): a view never restarts a chat whose last start failed A Claude chat whose CLI exits during startup left one red row per start, and every time a view bound to it (the chat opening right after its create died, or the user switching back to it) the hold started the CLI again, so the same launch-failure row repeated. Only a send retries a failed start now, the same rule provider-exit recovery already applied; the rule lives in one predicate the hold, exit recovery and the delivery loop share. * test(native-chat): start the child the loop waits on with an attach, not a second view A view no longer starts a child whose last start failed, so the R2 case that waits on a child started since the failure now gets that child from a client attach, the one non-send starter left. * fix(native-chat): settle a gone generation's turn wherever a conversation opens A send that opens a chat this process had not read yet (after a crash, from a phone or the CLI) went through the delivery open, which never settled what the dead generation left running; only the read restore and a successful acquire did. When the send's start then failed, the turn stayed running for every reader. The settlement now runs in the one journal open, at the crash boundary, for every opener except an acquisition, which settles from the evidence it read before its reserve; the read restore's separate step is gone. * test(native-chat): prove the next child's start settles the turn an earlier child left The R1 case lost its only settlement assertion when the latch it checked was deleted. It now seeds the running turn the earlier child left and asserts it ends at the exit's receipt, with the exit's row, before the message is handed to the new child. * test(native-chat): count a failed start's rows by row, not by text Comparing the set of texts passed when two different rows carried the same words, which is the duplicate the test exists to catch. * test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * test(native-chat): pin the open's and the send's start and row counts, however the view binds Opening a fresh chat whose starts fail makes one start and one row, with two views bound before or after the create's child died; one send makes one more of each. * fix(native-chat): settle a gone generation's turn at every open but an acquisition's The journal open skipped the settlement whenever the lease read reserved or live, to leave an acquisition's own open to the acquisition. But a lease a crashed process left in recovery also reads live, until the next acquire resolves it. A send that opened such a chat, from a phone or the CLI after a crash on a host that could not prove the old owner gone, skipped the settlement; when its start then failed, the dead turn stayed running for every reader. The acquisition now says it is the opener, and every other open settles, whatever the lease still claims. * test(native-chat): hold the create's start open until the views bind The "view binds while the create is still starting" case gave the create a 300 ms head start and asserted the views bound before it died. On a loaded runner the holds took longer, the create's exit landed first, and the case failed its own precondition. The create's initialize now waits on a gate the test releases once the views are bound. * fix(native-chat): Stop reads the one working rule every session list reads While Claude retries a rate-limited request it never echoes the message, so no turn opens: the sidebar read Working from the unanswered send while the composer showed Send. The chat's working state, the host's session-list status and the host's no-turn Stop check now call one shared rule instead of three copies. * test(native-chat): a rate-limit retry pins only that no turn opens, not how its rows are kept * fix(native-chat): Stop leaves a message waiting on its Retry, and does not show for one A send that failed holds the queue until the user retries it, and one the host restarted under is parked the same way. Stop counted both as still on their way, so it showed in an idle chat and could never go away, and pressing it dropped the failed message along with its Retry. * test(native-chat): the chat's Stop and a session list read the main agent alike over their own copies The chat reduces its stream and a list reads the status feed. Driven through the real host for a rate-limit retry with no turn, a subagent still running after the main turn, and the handed-over child exiting. * refactor(mobile): the chat reads the main agent's working state through the shared rule Behaviour is unchanged: the same two terms, now from the one function the host projection and the desktop chat read. * fix(codex): a Stop naming no turn never interrupts an earlier turn It fell back to the id an earlier turn/start answered with when the latest start went unanswered, or when the journal showed a compaction Codex had not started, and reported that as stopped. * fix(native-chat): a Stop naming no turn never says a turn had already finished When the provider found nothing left to stop, for instance a turn that ended between the host's check and the interrupt, the chat got "The provider had already finished this turn." for a turn the Stop never named. It now ends quietly, as a Stop with nothing in flight does. * fix(native-chat): one Stop the host could not settle no longer refuses every later one A Stop naming no turn has one operation key per session. When the host could not settle one, it answered every later Stop under the same id as unknown until the id expired. Once the host says so, the next press is a new Stop; transport doubt still replays the same id. * refactor(native-chat): drop the composer's second error formatter After the merge with main, every chat write in the composer path reports its failure as a typed outcome worded by the refusal-notice table, so the send's catch sees only a local throw. The {code, message} formatter this branch added for it has no payload left to format, and its claim to be the one way a chat words a failure is no longer true. The composer send is main's again. * test(native-chat): pin the reason on a message rejected while its chat was closed The reopen test checked only that the message reads as not sent; it now also checks the Retry row carries the host's reason. * test(native-chat): read Stop operation ids without a cast * fix(native-chat): a Stop whose answer was lost no longer swallows the next one A Stop that names no turn has one operation key per chat. When its answer was lost in transit, the chat kept the id, so every later Stop replayed it; the host answers a replay as already handled, so for up to a day Stop stopped nothing. The id is now dropped once the call settles, however it settles. A second press while the first is still on its way still shares its id. * refactor(native-chat): a Stop naming no target keeps its operation id only for its own call The chat kept each write's operation id per payload across calls, and dropped it only on some settle paths. That is right for a write naming what it acts on, but a Stop naming no turn, and a stop of every background task, share one payload with every later one, so any path that kept the id made the next Stop replay as already handled and stop nothing. One path was still open: an answer that arrived after the chat moved to a new fence. Whether a write names its target is now decided once, before its id is picked. One that names none keeps its id only while its call is in flight, so a press made meanwhile joins it, and releases it when the call settles, however it settles. The release runs only while the key still holds that call's id, so a joined call settling late cannot drop a newer one's. This replaces the per-path exceptions for a thrown call. * test(native-chat): read the Stop fences without a cast * test(native-chat): pin the new id for a named cancel the host could not settle After the Stop naming no turn moved to a per-call id, the only test of the unknown-refusal release was gone, and the half that stays, for a cancel naming its turn, could be removed with every test green. * fix(native-chat): a Stop pressed after a new message stops it, even while the last Stop is unanswered A Stop naming no turn shared its operation id with any press made while it was still in flight. The host runs a chat's writes in order, so a message sent between two presses was accepted after the first Stop ran, and the second press replayed that Stop as already handled and left the message running, although the chat had already withdrawn it from the outbox. A write naming no target now gets a new id on every press and is never kept, so each Stop acts on whatever is running when the host reaches it. A write naming its target keeps its id exactly as before. A double press can ask the provider to stop the same turn twice, which it tolerates. * fix(native-chat): Stop no longer blinks off as Claude opens the turn for a message Claude's echo of a sent message both answers the send and opens its turn. The echo settled the send first, so the host published the message as answered one frame before the turn it opened, and for that frame the chat read nothing running: Stop turned back into Send, and Working blinked off in every session list, for tens of milliseconds on each turn. The echo now settles the send after the turn it opens has been emitted, so the running turn is published first. * fix(native-chat): a message a Stop withdrew comes back to its sender's composer A Stop withdraws every message the host holds but has not run, and S also drops the ones this client had not handed over yet. Either way the message left the chat and its text survived only in a hidden journal row and the in-memory ArrowUp history. The sending client now puts the withdrawn text and images back in that pane's composer, after whatever is typed there. Withdrawn is read from the rejection reason through one shared check, which the outbox reconcile now uses too. The composer is written before the entry leaves storage, so a failure between the two repeats the text instead of losing it, and an entry storage no longer holds is never given back again, so a replay, a second view or a remount restores it once. Only this client's outbox holds the entry, so other viewers still see the message disappear. A failed Stop withdraws nothing on the host and gives nothing back. * fix(native-chat): withdrawn text put back during an IME composition is not lost While the IME owns the field, the composer ignores a programmatic draft, and the next composed keystroke wrote the draft without the restored text, after its outbox entry had already been dropped. The composer now holds text appended mid-composition, keeps it in the cache after each composed write, and shows it once the composition settles, the way attachments that land mid-composition already wait for it. * test(native-chat): pin that only a withdrawn message comes back to the composer * test(native-chat): set up the composer's window API for every describe in the composition-race file * docs(native-chat): note that the withdrawn check reads the legacy reason until a typed category lands * test(native-chat): pin that text put back mid-composition shows once, even beside a mid-composition clear * feat(native-chat): host-owned queued-message draft store in the session journal A queued mid-turn message is a draft row in the session's journal.db, created idempotently at every writable open with no user_version bump so a downgrade stays writable. Consume converts one draft into an ordinary submission inside the journal writer's own transaction (exactly-once), and a standing writer hook returns a consumed draft only when a committed row newly settles its current consumed submission to a non-withdrawn rejection — the same decision the reducer folds rows through. Open-time repair re-derives returned state behind the stored fact; retention never prunes a row whose refusal could still return it. * feat(native-chat): queued-messages wire contract, dark capability, and send classifiers The send result becomes a union: today's submission arm unchanged, plus a capability-gated queued arm only clients that sent delivery:'queue-if-active' ever receive. Whole-list queuedMessages fields ride the subscribe events and history pages; Stop gains withdrawQueued with the withdrawn bodies in its result; clear's result carries withdrawn drafts too. Both classifiers treat queued as accepted/spent. agent-session.queued-messages.v1 is defined but deliberately NOT advertised: the rollout prerequisites (Claude fold receipt, integrated Codex steer matrix) are not in this host. * feat(native-chat): queue a capable mid-turn send as a draft, drain it at turn end, and let Stop and clear return its text A send carrying delivery:'queue-if-active' while the session owes work — or behind an actionable backlog — becomes a host-held draft instead of a submission. A serialized drain woken by journal commits, draft mutations and conversation opens re-derives its gates from live facts (streamed-event barrier first, backlog never a gate) and converts the oldest actionable draft through the exactly-once consume; from that instant today's delivery pipeline runs unchanged. Stop pauses the withdrawable frontier at the stop step (a process-level pause set that survives handle eviction and, via the per-process host instance, restarts), then withdraws it with the text in the result for capable clients; /clear does the same for the superseded source. The draft list publishes whole per emit with identity dedup, rides only the final catch-up page, and attaches to history pages. queuedMessageSend overrides queue policy only; queuedMessageDelete hands the body back. Replays for all of it answer from op-stamped tombstone receipts. * test(native-chat): pin mid-turn queueing against the real host Accept (working/backlog/text-only/budget/replay), the one-per-settle drain, returned cards with N1 overtake and the N4 re-send loop, Stop withdraw with tombstone replays, the process-level pause across evict/reopen, Delete receipts, /clear returning the withdrawn text, and publication (hydration, unchanged-cursor insert, same-frame consume, identity dedup). * test(native-chat): read the queued receipt ids before the wait closures * chore(native-chat): SAFETY rationales on the sqlite row casts and a cast-free mobile narrowing * fix(native-chat): queued-draft bookkeeping never costs a publish, an open, a clear or a history read - Cache the draft list per draft-table revision. The drain re-checks on every journal publish, so each streamed delta was running a SELECT and parsing every draft body the handle had ever written (tombstones included). - Open-time repair/prune failures are reported and skipped; they no longer fail opening the chat. - /clear on a source with no drafts answers exactly as before: no empty `withdrawnQueued`, no empty write transaction, no extra publish. A draft read failure after the committed clear no longer turns it into a refusal. - History pages read drafts through the same guarded reader as subscribers. - Publication moves to its own module; the held-draft rule lives with the pause state; one pending-prompt check; drop an export nothing calls. - Tests: restart-held drafts, pre-consume failure pause + Send retry, failed open repair, clear with no drafts. * fix(native-chat): a Stop that withdraws a consumed draft's send gives its text back A queued draft converted into a submission leaves the sender's outbox, so when a Stop withdrew that submission before the agent received it, the text had no holder: the draft stayed `dispatched` forever and nothing restored it. - The returned-card rule now follows every effective `rejected` settlement of a consumed draft's submission, a Stop's withdrawal included, with the withdrawal reason stored as the fact (`dispatchWasWithdrawn`). The writer hook and the open-time repair share the rule, so no rejected submission can leave its draft `dispatched`. - A capable Stop withdraws the cards it returned itself along with its frontier, stamped with its caller-scoped key: the text comes back once in `withdrawnQueued` and replays from the tombstone. An old client's Stop leaves a returned card. - Stop's draft steps move to structured-agent-session-queued-stop.ts. - Tests: Stop between consume and the agent's receipt for both client kinds, its replay, a crash after the withdrawal, restart in the window, and the repair of a hookless withdrawal. * perf(native-chat): the queued-draft drain takes no serialized step while the agent works The drain was woken by every journal publish and, with a draft waiting, queued a serialized step (streamed-event flush included) per publish, only to find the session still working. During a streamed turn that is one step per delta, contending with Stop and every other mutation for the session's queue. The pre-check now also skips while the session is working. Whatever ends the work is itself a commit that schedules again, and the step still re-reads every gate after its flush, so no wake is lost. - Test: queued sends during a turn take no drain step; settling the turn drains. * fix(native-chat): a clear withdraws queued text only for a caller that can take it back; paused reasons are markers An older client running /clear had its source's waiting and returned drafts withdrawn and their text returned in a `withdrawnQueued` field it does not read, so the text was lost. Clear now mirrors Stop: `withdrawQueued: true` on `agentSession.conversationCommand` (strict params, sent only when the queued-messages capability is advertised) withdraws the drafts and returns their text once, replaying from the tombstones. Without it the source keeps its cards: the supersession fence already blocks the drain, and Delete still hands the text back. A paused card's reason was host-authored English on the wire. It is now a typed marker (`send_failed`) the client localizes, like `returnedReason`; a client treats an unknown marker as a plain pause. - Tests: an old client's clear leaves the cards and its replay stays field-free, then Delete returns the text; a capable clear returns the text once and replays it; the paused marker. * fix(native-chat): a draft pause that commits no journal row still reaches live subscribers A pause writes no journal row, so it reaches subscribers only on the next publish. Two pauses had none behind them: the drain's pre-consume failure (the session is idle by then, so nothing else commits) and an old client's Stop that interrupted nothing. A live card kept reading as waiting, with no failure marker, until some unrelated commit arrived. The drain now publishes after pausing a draft it failed to convert, and an old client's Stop publishes when it paused a frontier. - Tests: a failed conversion and an idle old-client Stop each reach a live subscriber as a paused card; both fail without the fix. * fix(native-chat): a failed clear wakes the queued drain, a failed Stop withdrawal still publishes its pause A conversation command can settle on the record alone (a retried clear that fails), so drafts held behind its prepared phase waited for an unrelated journal commit; the command controller now re-derives the drain when any command finishes. A capable Stop whose withdrawal write failed never published the pause it set, and a publish failure after a committed withdrawal (Stop or clear) dropped the bodies from the answer; publishing now happens outside the withdrawal and can no longer discard its result. Tests reset the process-level pause set between cases: operation ids repeat per test, so a shuffled order held later tests' drafts. * refactor(native-chat): the draft store notifies through the journal's commit listener, the hold is a stored row fact, and one typed gate decides every queue hold R1: every standalone draft-table transaction that changed rows (insert, withdraw, hold, open-time repair) fires the journal's own commit listener after COMMIT, so a draft or hold change publishes and wakes the drain through the same path a journal row does — no call site can forget. All hand-written publish/wake plumbing for draft changes is deleted; wakeQueuedDrain survives only as the record-input wake (a conversation command can settle on the record alone). R2: the process-level pause set becomes a hold_reason column on the draft row (pre-ship, so no migration): holds survive eviction and restart, keep their send-failed marker across restarts, die with the session's journal, and are cleared by consume and withdraw in their own UPDATE. The host-instance derivation stays the one restart mechanism. R3: one typed structuredQueueHold (blocked | command | prompt | working) consumed by admission, the drain step and Send-now, with each caller's override set written beside it. A capable send during a late-result /compact now queues instead of being refused (PLAN §3.1); the dead prepared-command branches and the drain's duplicated gate list are gone. prompt outranks working so Send-now's one override cannot swallow it. R4: one isUnsettledQueuedMessage predicate for the withdrawable/budget filters. Loop 4: a replayed send whose draft was refused answers with the returned card, never the rejected submission, so the text cannot render twice. Rewind completion was verified to publish after the record clears (the rewind path's own publish; the open path's recovery precedes the open snapshot). * fix(native-chat): a Stop with no drafts writes nothing, and a failed hold still lets a capable Stop withdraw The stored hold turned Stop's in-memory pause into a draft-table write, so every Stop (drafts or not, capability advertised or not) opened a BEGIN IMMEDIATE/COMMIT. An empty hold now returns before the serialized write. A hold that threw also emptied the frontier, so a capable Stop withdrew only returned cards and left the waiting drafts unheld to auto-send after the interrupt. The frontier is read once and survives a failed hold. * fix(native-chat): a capable Stop with no drafts writes nothing The empty-hold guard from the previous fix did not reach withdraw, so every capable Stop still opened a write transaction after the interrupt, and a closed handle turned its empty answer into a missing field. The draft store now answers an empty withdraw without a transaction, for every caller. * refactor(native-chat): Stop and /clear never withdraw queued drafts; no text rides the wire back Adopt the host-owned-queue model end to end: a Stop holds the waiting frontier ('stopped') for EVERY client and interrupts — the cards stay published as paused, Send-now overrides per card, and the pause dies when the user next starts a turn (an ordinary dispatched send lifts 'stopped' holds in the same serialized step; 'send_failed' holds still need their explicit Send). /clear carries the source's unsettled drafts to the replacement session as born-held rows — identical for every client version — then tombstones the source. Delete answers with no body: the card leaving the published list is the outcome. Removed (never shipped; the capability was dark and unadvertised, so no wire compatibility is affected): CancelParams.withdrawQueued and its refine, ConversationCommandParams.withdrawQueued, CancelResult.withdrawnQueued, ConversationCommandResult.withdrawnQueued, AgentSessionWithdrawnQueuedMessage, the Delete result body, settleStopQueuedWithdrawal and the cancel finisher, withdrawClearedSourceQueuedMessages, replayWithdrawnQueuedMessages, and cancelPlan's tombstone replay. This also removes the defect where a withdrawal took every row regardless of which client sent it (a phone Stop pulled desktop-typed text): nothing moves text anymore, so a Stop from one client can never relocate another client's drafts. Hold and carry writes are bookkeeping: a failure is logged and never gates the interrupt or the clear. * feat(native-chat): a restart hold lifts like a Stop's, and paused cards say why The user's next dispatched send lifts every stop-shaped hold in one UPDATE: stored 'stopped' rows, and restart-held rows (host_instance mismatch), which are adopted into the running instance — the same fact the derivation reads, so no second copy of the hold exists. 'send_failed' still requires its explicit Send. Publication now marks stop/restart holds with pausedReason 'stopped' (an additive optional value on a dark capability), so clients can caption them "sends after your next message" and keep "couldn't send" for 'send_failed'. * fix(native-chat): only a client's own send lifts a Stop's queue pause The lift ran for every accepted host send, so orchestration mail, a restart continuation and a launch prompt released drafts the user had stopped (and adopted restart-held rows into the running instance). The client-facing agentSession.send RPC now marks its sends as the user's own; host-internal senders leave the pause alone. Also drops comments still describing the withdrawn return-text rule. * fix(native-chat): a Stop's queue pause lifts when the user's send starts its turn The pause lifted as soon as the host accepted a user send, so a send the provider then refused (a failed child start, a refused turn/start) had already released the stopped drafts into the same failure. The host now remembers a client's own send, in memory, until the provider answers it: acceptance lifts the stop-shaped holds, a refusal forgets it with the holds intact, and a later Stop supersedes it. Nothing is persisted, so a restart between the send and its turn start leaves the cards held for the user's next send rather than sending them unasked. * fix(native-chat): a consumed draft's turn starting lifts a Stop's queue pause Drafts are only ever a client's own sends, so a drained draft or a Send-now is a user send for the pause: its submission joins the same in-memory set a direct send uses, and the provider accepting it lifts the stop-shaped holds. Before, a message typed while a stopped turn wound down drained as a draft and left the older stopped cards held, so their "sends after your next message" caption was false. A refused consumption lifts nothing, a later Stop still clears the set, and orchestration mail and restart continuations still never lift. * fix(native-chat): queue a capable send behind a /compact and re-scope /clear's carried drafts - A text send with queue-if-active during a /compact in flight is admitted on the compact's side lane as a held draft instead of being refused; it may only become a draft, so one the gate no longer holds is refused rather than dispatched. - Drafts /clear carries to the replacement are fingerprinted for the replacement session, so the provider's echo folds into the sent bubble. - The in-memory set of user sends awaiting their turn is capped; sends settling unknown no longer grow it without bound. - Correct the userSend comment: the renderer's launch prompt goes through the client RPC and does set it. * feat(mobile): render host-queued drafts as cards with Send-now/Edit/Delete, and restore withdrawn text once across reload * feat(mobile): /clear withdraws queued drafts on capable hosts, and reason markers map to readable copy * fix(mobile): an empty queued-draft publication never churns the held empty list * fix(mobile): relaunch recovery replays results only — a persisted Stop or /clear never re-executes * fix(mobile): a relaunch never reissues a Stop or /clear, a lost withdrawal answer is re-asked, and the send journal stays readable by older builds Relaunch recovery no longer recomputes the host's replacement-session id: that copy of the host's derivation had already drifted (full digest vs the host's 40-hex slice), so it could never fire. A previous process's Stop and /clear handles are now released once per pane per process — the host keeps unwithdrawn drafts visible as cards — and only an Edit is finished. The sweep runs once per process in one serialized journal step, so a remount can no longer drop the handle of this process's own in-flight Stop and lose its text. A withdrawing Stop, /clear or Edit whose answer is lost is re-asked under the same operation id (bounded): once the host committed, the card is gone and only that answer carries the text back. `delivery` is no longer a persisted send-journal field — an older build (or an older host's page, which reads the same key) would find the strict schema unreadable and refuse every structured send. It is part of the intent key instead; the immediate key is unchanged, and a retained entry under either key replays exactly as first sent. An identical send whose retained id replays as a withdrawn draft goes out under a fresh id instead of vanishing. Also: the send callback is stable across streamed frames, card busy state is per card, card actions carry a button role, and render-time ref writes moved into layout effects. * fix(mobile): a lost-answer queued send never reads as unconfirmed, the restore journal works inside the page, and its handles die - A send whose answer was lost but which the host holds as a queued-draft card no longer warns "Delivery unconfirmed" after 20 s: a card that was not on screen at send time with the send's text counts as delivery, like the transcript echo does. - The queued-draft restore journal key joins the page storage allowlist and writes through the mirrored path, so a Stop, /clear or Edit from the page-served session screen keeps its replay handle; a write the store dropped without rejecting is refused up front so the caller restores directly instead of holding a handle no store kept. - Seeing a published draft named by a journaled send's operation id spends that entry: the draft is the host's receipt of the send, so a draft later withdrawn elsewhere no longer leaves an entry nothing will ever settle. - Withdrawn text is restored at most once even if removing the journal entry fails after the restore ran. - A returned card offers Edit, as on desktop. - Test outcome annotations use MobileNativeChatSendOutcome, which now includes 'queued'. * fix(mobile): withdrawn queued text reaches its pane even after the screen closed, and a lost answer is re-asked in the background A Stop, /clear or Edit answered after the session screen closed wrote into a composer that no longer existed, while the journal entry was removed: the text was lost. Restored text is now owed by composer scope and the pane's composer takes it once whenever that scope is active. A lost answer was re-asked inline up to three times, each with the full command budget, so a /clear could hold the composer for minutes. The first answer now returns at once and re-asks run in the background on the 1 s / 2 s / 4 s schedule with a short budget each. Restoration stays once per operation id, also when a retry of the same id races the re-ask. * refactor(mobile): queued drafts stay host-owned — Stop and /clear never pull text back to the phone A Stop or /clear no longer withdraws queued drafts and ships their text back over the wire. Cards stay on the host as paused cards (captioned 'Paused — sends after your next message') with Send / Edit / Delete, identical on every device, and the host carries them across a /clear itself. Edit copies the card's shown text into the composer first and then issues a plain Delete, so no RPC outcome can lose it; a failed Delete leaves the card visibly beside the copy. With no text in flight there is nothing to make exactly-once: the AsyncStorage restore journal and its page-storage allowlist entry, the background re-ask schedule, the relaunch release/replay pass, the restore-once guards, and the owed-composer-text buffer are all deleted. Cancel and conversationCommand go back to main's plain requests, so old and new hosts see one Stop and one /clear behaviour. Kept: capability gating, the cards and captions, Send-now, the 'queued' send outcome, the queued-card settlement of unconfirmed sends, the delivery-in-key send-journal fix, and the queuedMessages frame tracking. * fix(mobile): only a 'stopped' hold promises "sends after your next message" The host now publishes pausedReason 'stopped' for Stop, /clear-carry and restart holds. An absent or unknown marker is a hold whose release rule this build does not know, so it reads as a plain "Paused" instead of promising that the next message resumes it. Drops an unused type re-export. * fix(mobile): a paused queued card's action reads Send, not Send now "Send now" names jumping the running turn. A paused card (a Stop, /clear or restart hold, or a failed conversion) waits on no turn, so like a returned card its action and accessibility label say plain Send, matching desktop. * test(mobile): find the queued card's Text nodes by name so the test typechecks * fix(mobile): retire a replayed send the host refuses by shape; 44pt queued-card targets A retained delivery send that an older host's strict schema turns away can never be accepted, so keeping its operation id refused every later send of the same text. The host's own request refusal now retires it; any other doubt still keeps the id. Queued-card actions now touch as 44pt targets while drawing as a 32pt text row. * fix(mobile): a waiting queued card's action reads Steer, as on desktop The same action read "Send now" on the phone and "Steer" on desktop. Its accessibility label now matches the desktop hint: "Send now without waiting for the turn to end". A paused or returned card keeps plain Send. * fix(mobile): focus the composer after Edit moves a queued message into it Edit copied the card's text into the composer but left it unfocused, so the user had to tap the field to keep typing. The composer now takes an input ref and the chat view focuses it on the next frame after Edit. * fix(mobile): only a schema rejection retires an in-doubt send; an auth refusal keeps its id An unauthorized answer to a replay says nothing about whether the first attempt was delivered, so retiring its id there could send the message twice. * fix(native-chat): a returned queued card carries the typed rejection fact, like a rejected submission A consumed draft the agent never ran comes back as a returned card. The card kept only the rejection's sentence, while its submission now also records the typed fact a client classifies from. A host-restart rejection's sentence carries no legacy marker, so such a card could not be told apart from a provider's refusal. The draft table stores the submission's fact next to its reason (`returned_rejection`, written by the same settlement that sets the reason, and read back with the reducer's own fact reader), and the card publishes it as `returnedRejection`. Both are overwritten on every return, so a re-sent card never keeps an earlier refusal's fact, and a /clear carry inserts a plain held draft with neither. Retention moves to queued-message-retention.ts to keep the table module within max-lines. * fix(native-chat): fit the queue to main's typed rejections and compaction result Main (#23026) dropped the disposition's fresh-id retry field, gives a rejected dispatch a typed sentence plus fact, and types /compact's result. The queued-draft disposition and the queue tests now use those shapes. * fix(mobile): a returned queued card reads its typed rejection like a rejected send The card's label called `dispatchRejectionReasonIsInternal`, which main removed when rejections became typed facts, so labelling a returned card threw. A host-restart rejection's reason is also a sentence now, with no marker for the old string check to recognise. The label now comes from the shared words a rejected send gets (`structuredAgentSessionRejectionParts`, 'composer-send'), given the card's `returnedRejection` fact and falling back to `returnedReason` when a host wrote none. A Stop withdrawal, read through `dispatchWasWithdrawn` with the fact, keeps "Held back by Stop — Send to retry"; a provider's words show only where their audience is the person; a fact kind this build cannot place reads as not sent rather than trusting the sentence beside it. * fix(mobile): a returned queued card is worded as the desktop card words it The card has its own Send, so its words leave out sending again, the way the desktop card passes retryControl to the shared attempt-failure words. A Stop withdrawal reads "Stopped before it was sent", the desktop caption. * fix(mobile): a returned card with a fact this build cannot place shows the host's sentence The host writes a rejection's reason as a sentence for a person, so when a newer host's fact kind cannot be placed, that sentence is the best words available. The card now leaves it to the shared attempt-failure words, as the desktop card does; an old internal marker still maps to generic words there. * fix(native-chat): draft bookkeeping can never roll back the journal row it rides The queued-draft returned transition runs inside every journal append's transaction. A throw there (a draft table an earlier build created without the returned_rejection column) rolled back the journal's own rejection row, so a Stop, a failed start or a provider refusal could not be recorded. The standing hook now runs in its own savepoint: its failure is logged and rolls back alone, and the open-time repair re-derives the missed transition from the committed row. The draft table also gains any missing nullable column at open. * fix(mobile): a returned card's reason reads whole, and Edit never deletes text it did not copy A returned card's label was capped at one line, but its reason often reads only at its end ("... does not support the image type .bmp in a steering message."). It now wraps; waiting and paused holds stay one line. Edit copied a card's text into the composer and then deleted the card, whether or not the copy landed: before the composer mounted, or for an empty card, the append was a no-op and the delete still ran. The append now reports whether it copied; Edit stops before Delete when it did not, and focuses the composer only after a copy. When Edit's Delete loses to the drain, the phone says "Already sent — your text is still in the composer.", as the desktop does, so the copy is not sent a second time. The controller now carries the queued controls as one field, which keeps it within max-lines. * fix(mobile): a queued send's replay never vanishes or paints an unretired bubble A replay answered `withdrawn` is resent under a fresh id only when the retained id could be released. When the release failed, the answer fell through as `queued`, which shows nothing, and the text was gone. It now reads as rejected, so the text returns to the composer. A replay answered `queued{state:'dispatched'}` was mapped to `accepted`. The host answers that way only when it cannot find the submission the draft became, so no echo would retire an optimistic bubble. It now maps to `queued`, which shows nothing; the transcript or the card owns the text. * fix(mobile): a queued card hides once its message arrives, and a failed pause says tap Send A waiting card whose submission has already arrived is hidden, as the desktop card projection hides it: on a multi-page catch-up the shrunk list rides only the final page, so the bubble and the card showed together. Returned cards always show. The send-failed pause reads "Couldn't send — tap Send to retry". * fix(native-chat): a draft a Stop or restart took back waits again instead of blocking the queue Cards A, B and C wait; the turn ends and the drain consumes A, but the agent has not taken it yet. A Stop then pauses B and C and withdraws A's submission, which made A a returned card. The user's next send lifted B and C, yet a returned card blocks everything behind it, so B and C never sent although they read "sends after your next message". A restart or close before hand-over did the same. Nobody failed the user there, so the draft now goes back to waiting at its own position, under the hold that same event put on the drafts behind it: a Stop's 'stopped', or no stored hold after a restart, whose hold derives from the host instance. It carries no refusal, and records its spent submission id in consumed_as, so its next consume (the drain, or Send on the card) mints a fresh id through the same path a returned card's re-send uses. Provider refusals and other failures still return the card. The live settlement hook and the open-time repair share one decision. After a Stop and the user's next turn, A drains first, then B, then C, one per turn. * fix(native-chat): Delete and Send on a queued card answer at once during a /compact A /compact holds the chat's serialized lane for its whole provider call, and the queued-card Delete and Send ran on that lane, so both hung until the compaction finished. They now run on the side lane a draft-only send already uses while a compaction is in flight: Delete completes at once, and Send reaches its readable "wait for the conversation operation" refusal at once. The drain stays on the main lane and keeps its command hold, so nothing sends until the compaction settles. * fix(native-chat): a re-sent returned card drops the refusal it came back with Re-consuming a returned card left returned_reason and returned_rejection on the now-dispatched row, so the row described a refusal that no longer applied. The consume clears both in the same update that moves the card to dispatched. * perf(native-chat): the queue gate reads pending prompts without rendering the journal The prompt check ran on every send admission and drain step, and read journal.snapshot(), which copies and sorts every item in the chat. It now walks the reduced items in place with journal.visitItems; the answer is the same, since the snapshot only sorts those items. * fix(native-chat): a Stop that fails leaves the queued cards as it found them Stop holds the waiting cards before it withdraws queued sends and interrupts the agent. When a later step threw or the Stop was refused, the cards stayed paused ("sends after your next message") although a failed Stop is meant to change nothing. A failed Stop now undoes exactly what it added: each card it held gets back the hold it replaced, a consumed card its withdrawal sent back to waiting is released, and the user sends it had set aside can again lift the pause. Holds an earlier Stop or a restart put on the cards stay. The hold SQL moves to its own module, and the draft store's standalone transactions share one helper. * docs(native-chat): confirmed cancellation is no longer a queue rollout prerequisite Stop withdrawing queued sends with a typed cancellation landed on main with #23026. The comment gating the queued-messages capability now lists only what remains: the Codex steer matrix (#21062), the Claude fold receipt, turn-owner bars, and the desktop and phone clients. * docs(native-chat): the Claude fold receipt and turn-owner bars have landed; Codex steer and the clients remain * test(mobile): compare the queued card's Text nodes by name so the test typechecks * fix(mobile): a draft a Stop requeued stays visible beside its rejected first submission A Stop that withdraws a consumed draft before delivery puts it back to waiting under its own id, while its first submission stays in the journal as rejected. The card filter hid any waiting draft with a same-id submission, and the transcript hides rejected submissions, so the requeued text could not be seen, edited, deleted or sent, and later drained unannounced. A rejected submission no longer hides a card: beside a waiting draft it can only mean a requeue, since a refusal returns the card instead. * fix(mobile): a send record storage will not clear never blocks sending that text A replay answered `withdrawn` is resent under a fresh id only after its saved record is cleared. While storage kept failing to clear it, every send of that exact text replayed the old id, was answered `withdrawn`, and came back rejected, so the text could not be sent until storage recovered. The resend now goes out once under a fresh id that bypasses the saved record, and the failed clear is reported through the send's error path. * fix(mobile): a send record whose draft went out under another id is spent A send whose answer was lost keeps its record, so the same text replays the same id. When a Stop requeued that draft and it later drained under a fresh id, nothing could clear the record: the draft was no longer published, and no submission carries the old id. Every later send of that text replayed it and waited for a settlement that would never come. The host answers that replay with the submission the draft became, under a different id than the one replayed. That answer now spends the record. Which send the phone meant stays unconfirmed, as for any retained replay of a live send. * fix(native-chat): Send on a queued card during a /compact is refused before it takes a lane Send-now chose its lane once, at entry. During a /compact it took the side lane, where it could wait behind a Stop, then run after the compaction had settled and append a real submission unserialized against the main lane. While a compaction is in flight, Send-now is now answered with the "wait for the conversation operation" refusal before entering any lane, and otherwise it runs on the main lane. Only Delete keeps the side lane, whose compare-and-set withdrawal is safe on either. * fix(native-chat): a Stop that fails after reaching the agent keeps the queue paused A failed Stop undid its queue holds whenever it threw, including after the interrupt had already gone to the provider (a status-note write failing after cancelTurn, or after stopping a starting agent). The turn could be stopped while the cards drained as if no Stop was pressed. The Stop now marks the step that reaches the provider, and undoes its holds only when it failed before that. A Stop the agent refused answers ok and keeps its holds; the comment no longer claims otherwise. * fix(native-chat): a skipped draft settlement heals on the next drain step, not only at reopen The draft settlement rides each journal append as bookkeeping, and a failure there is logged and skipped. Only the open-time repair re-derived it, so a consumed draft whose submission was rejected stayed dispatched (invisible, and blocking nothing it should) until the chat reopened. The re-derivation is now its own function, shared by the open-time repair and the drain: whenever a dispatched draft's submission is already rejected, the drain step applies the owed settlement first. * fix(native-chat): one id is never recorded as a submission twice A second submission row under an id the journal already holds replaces the submission with a fresh pending one, so a rejected message could be handed over again under its own id. Send on a queued card could do exactly that: if the host died after it consumed the card under the operation's id but before its answer settled, the rerun consumed again under the same id. The journal now refuses a submission under an id it already records, so no id is delivered twice whatever the caller does. And a Send-now rerun that finds the card consumed under its own operation id answers with that submission instead of consuming again. * fix(native-chat): a waiting draft whose first send the agent echoed is withdrawn, never resent A consumed draft goes back to waiting when its submission is rejected as never delivered (a Stop's withdrawal, a restart, a close), and then sends again automatically. That rests on the "never delivered" claim. If the provider then echoes that message, the first delivery happened, and the automatic resend would give the agent the same message twice. The reducer already keeps such an echo apart, since a rejected submission may not claim it, so the draft store reads it from the appended row itself: a provider echo of a user message that no live submission claims, matching a waiting draft whose spent submission is rejected, withdraws that draft the way a Delete would. The echo-claiming rule is split out of the reducer's aliasing so both read the same decision, and the per-row draft hook moves beside the settlement re-derivation. * feat(native-chat): a submission names the queued draft it hands off Clients told a queued card's hand-off apart from other sends by comparing the draft's id with the submission's id. That holds only for a draft's first hand-off: a re-send, or a draft that goes back to waiting and drains again, goes out under a fresh id, and the clients showed the card and the sent message together, or restored text the host still held. Every submission the host creates by handing off a draft now carries queuedMessageId, the draft's id. It is written on the submission's journal row as an optional key (older readers keep it and ignore it), carried by the reducer, listed in the published submission schema (which otherwise strips it), and stamped where the row is built from the consume itself, so no hand-off path can leave it off; a caller naming a different draft is refused. A direct send names none. The queued-messages capability comment makes the link part of v1. * refactor(native-chat): every queued draft goes out under a fresh submission id A draft's first hand-off reused the draft's own id as the submission id, so comparing a draft id with a submission id looked right in every first-send test and failed only on a re-send or a requeued draft. Every hand-off now uses a fresh id (the drain mints one; Send on a card uses its operation's id), so id equality is never true and a reader must use the submission's queuedMessageId. The host gets simpler: queuedMessageNeedsFreshSubmissionId is gone, consumed_as is set on every dispatched row and cleared when a withdrawal sends the draft back to waiting (its spent submissions stay findable by their link), the consume refuses the draft's own id, and the consumedAs ?? messageId fallbacks collapse. The delivered-echo check finds spent hand-offs by link. A send this host queued, asked again (a lost answer's replay, or a rerun the operation ledger no longer covers), answers from its draft and then from the hand-off that names it, through one function. The rerun path used to be kept from sending twice only because a submission sat under the send's own id; with fresh ids that guard is now explicit. A Send-now rerun recognises its own consume by the link instead of consumed_as. * fix(mobile): relate a queued draft to its hand-off only through queuedMessageId The phone matched a draft to the submission it became by comparing ids. The host now hands off every draft under a fresh submission id and stamps that submission with `queuedMessageId`, so the ids never match and the link is the only relation. - A waiting card hides exactly when a submission names it as its `queuedMessageId` and was not rejected; a rejected hand-off is what sent the draft back. A direct send sharing the card's id hides nothing. - A saved send record is spent when its id is a published draft id, or when any submission names it as its `queuedMessageId`, in whatever state. This clears a lost send that a Stop requeued and the drain later sent under a fresh id, from the live submissions stream. - The replay rule that spent a record when the answer's submission id differed from the replayed id is gone. A replay answered by the hand-off reads as unconfirmed, as any retained replay of a live send does, and paints no optimistic bubble; the stream spends the record once it carries the link. * fix(native-chat): an echo withdraws a draft only if its rejected hand-off reached the agent The delivered-echo rule withdrew a waiting draft when a provider echo matched any rejected hand-off of it, including one a Stop rejected before it was ever handed over. That hand-off is provably unwritten, so a matching unclaimed echo is some other message, and the rule silently deleted the card. Only a hand-off that was handed over and then rejected as never delivered can be disproved by an echo now. * fix(mobile): a replay the host answers with its draft's hand-off spends the send record A retained send replayed after its queued draft was handed off is answered by the hand-off, which names the replayed id as its `queuedMessageId`. That answer fell to the retained branch and kept the record, leaving only the live stream to spend it. The stream can miss the hand-off for good: a reconnect snapshot covers only the latest page, and submissions ride only with their items. Every later send of that text then replayed and read "Delivery unconfirmed" forever. The answer states the link, so it now spends the record directly, checked before the rejected and retained branches. Which send the phone meant stays unconfirmed, as for any retained replay of a live send. * fix(mobile): a resend past an uncleared send record replays its own id on a retry When storage could not clear a withdrawn send's record, the phone resent the text under a fresh id with no record. If that resend's answer was lost and the user sent the text again, the old record replayed, came back withdrawn, and the phone minted another fresh id: a second delivery if the first resend had landed. The resend's id is now kept in memory for the app run, by operation key, so a retry replays it; it is forgotten once the host answers it as spent. The "couldn't update its record" notice now shows only when the resend is known to have gone out (accepted or queued), never for an unconfirmed one. * fix(native-chat): a skipped echo withdrawal is re-derived before the draft can send again The delivered-echo withdrawal rides each journal append as bookkeeping, and a skipped hook left the draft waiting, so it later sent the same message a second time. Nothing re-derived it. The draft store now also withdraws, in its owed-settlement pass, each waiting draft that an echo already in the journal proves delivered: an unclaimed provider user message (still stored under its own id), carrying the draft's payload, appended after a hand-off that was handed over and rejected. The live hook and the re-derivation share one predicate. The pass runs at open and in the drain step, right before a draft would send; it reads every item, so it never runs per streamed row. * fix(native-chat): a rolled-back journal append leaves no draft state cached The draft store caches its row list by revision. The per-row hook read that list eagerly inside the append's transaction, after the consume in the same transaction had already written and bumped the revision, so a failed COMMIT left the cache showing a hand-off that never happened. The hook now reads the drafts only once a row holds an unclaimed echo, and any rollback of a journal append or of its bookkeeping savepoint invalidates the cache, so no other read inside the transaction can leave it stale either. * fix(native-chat): a replay of a deleted queued card answers withdrawn, not refused Once a deleted card's tombstone is pruned, a replay of the send that queued it found the draft through its last hand-off. When that hand-off had been rejected (the card came back, and the user then deleted it), the replay answered with the rejected submission, which clients show as a failed send with a Retry. Only a withdrawn row is pruned while its last hand-off stands rejected, so the replay now answers queued, withdrawn. * test(mobile): a withdrawn replay of a pruned deleted card, position 0, still frees the text The host now answers a replay of a deleted card whose row was pruned with queued{state:'withdrawn', position: 0}. The phone reads only the state, so the next identical send still goes out under a fresh id; the test now uses that receipt. * fix(mobile): a bypassed resend's id survives storage recovering, and a malformed answer spends nothing The remembered id of a resend sent past an uncleared record was consulted only while the record still could not be cleared. If storage recovered between that resend's lost answer and the retry, the record cleared, the normal path minted a fresh id, and the message could be delivered twice. The retry now hands the remembered id to the new saved record, which replays it, and memory lets it go. The id is also keyed by the operation key the saved record matched, not the delivery asked for now, so a capability change in between cannot mint a fresh id. A send answer is read as its draft's hand-off only when it carries a submission, so a malformed answer with neither a submission nor an id no longer spends the saved record. * refactor(native-chat): name the queue's pause-lift for what it releases * refactor(mobile): name a schema-refused replay for what the host decided * chore(native-chat): one import of the mutation helpers * refactor(mobile): build the queued-card slot outside the chat view MobileNativeChatView was over its 400-line limit with the queued cards wired in. The cards and the composer ref their Edit focuses are now built by useMobileNativeChatQueuedSlot in the overlay, and the view only places the cards and hands the ref to the composer. * test(mobile): say why the overlay test's partial controller is safe * feat(native-chat): a Stop pauses the whole queue, derived from the journal, with an explicit Resume After a Stop, each waiting card was held on its own row ('stopped'), lifted when the host saw, in memory, that a user send made after the Stop had its turn accepted. The cards read "sends after your next message" one by one, there was no way to resume the queue without sending something, and the in-memory record of user sends was lost on a restart or eviction. The pause is now the queue's, and derived rather than stored as a flag: - 'stopped': the user's last Stop took effect at a recorded journal position and no turn a person asked for has started since. "A person asked for it" is the new `origin: 'client'` on the submission row (a send over the client send RPC, or a card they sent now); orchestration mail, a restart continuation, a host-sent launch prompt and the queue's own drain record `host` and never lift it. - 'restarted': a waiting card was written by another host process and no person's turn has started since this conversation opened. Resume (`agentSession.queuedMessagesResume`) lifts either. Send-now sends one card; the rest stay paused until that card's turn starts, which is a person's turn like any other. The journal's row kinds are closed (an older build truncates a journal at a row kind it does not know), so the one event the journal cannot carry, where the Stop took effect, is recorded beside the drafts in `queued_message_pauses`; everything after it is read from the journal. A Stop records it only once it takes effect (after withdrawing queued sends, as it reaches the agent), so a Stop that fails first leaves nothing to undo, and the per-row hold, its undo and `userSendsAwaitingTurn` are gone. A card keeps a hold of its own only when its conversion failed ('send_failed'). The pause is published once, as `queuePause` beside `queuedMessages`, on live frames, catch-up and history. A /clear starts its replacement paused, as after a Stop, since the carried cards were written for the context it discarded. * feat(native-chat): a /clear's replacement queue reads paused because of the clear, not an interrupt The replacement's pause was recorded as 'stopped', which clients show as "Queue paused because you interrupted" although the user cleared the chat. It is now its own reason, 'cleared', on queuePause.reason ('stopped' | 'restarted' | 'cleared'). It lifts and resumes exactly like a Stop's: through Resume, or the user's next turn starting on the replacement. * feat(mobile): a paused queue shows one header row with Resume, and its cards keep Steer The host now pauses the whole queue after a Stop, a restart or a /clear, and publishes that pause once beside the list instead of on each card. The phone followed the old per-card pause: each card read "Paused — sends after your next message" and its Steer turned into Send. - One header row above the cards says why the queue is paused ("Queue paused because you interrupted", "... because Orca restarted", "... after you cleared the conversation"; an unknown reason reads "Queue paused") and offers Resume, which calls agentSession.queuedMessagesResume through the same mutation path as Delete, so a refusal reaches the error banner. It shows only while there are cards. - Cards under a paused queue keep Steer, Edit and Delete and read "Queued", promising no send time. A card whose own send failed still reads "Couldn't send — tap Send to retry" with Send, and returned cards are unchanged. - Steer's accessibility label is "Submit without interrupting the model". The pause is kept per session beside the list and changes only on frames that publish the list. The row lives in the queued-cards component, so the chat view gains no lines. The queued hook tests' published shapes move to a fixture module to keep that file within its line limit. * test(mobile): the paused-queue header words the host's 'cleared' reason The host now sends queuePause.reason 'cleared' after a /clear carries the queue over. The header already worded it; the tests now pass it as the host's typed reason rather than as an unknown one. * test(mobile): import the subscribe-event type the queued hook test names * fix(native-chat): a queue pause covers only the cards it paused A Stop recorded its pause fact even when the queue had no cards, and the fact outlived the cards it did pause. The published list hid a pause over no cards, but the drain still treated the queue as paused, so a card typed much later — during an orchestration-mail turn, or a correction typed before the stopped turn ended — sat under "paused because you interrupted" with no Stop of its own. A Stop now records its pause only if the queue holds a card when the Stop takes effect (the hand-offs its withdrawal sent back included). The fact is retired in the same transaction as the Delete, consume or withdrawal that empties the queue, never from an async publish. A /clear's carry now lands each card with its 'cleared' pause in one transaction, so a failed insert leaves no pause over an empty replacement. * fix(mobile): the paused-queue row keeps Resume's whole target and is announced - The row no longer pulls itself past the top of the card list with negative margins. The list has no padding there, and Android drops touches outside a parent, so part of Resume's 44pt target could not be tapped. - The row is a polite accessibility live region, as the app's other notices are, so a screen reader hears that the queue paused. - A pause published with no reason, or one this build does not know, now counts as a pause. The reducer compared reasons only, so it kept a previous "not paused" for it; it now reads "Queue paused". - The card list is keyed by conversation, so a Resume still in flight in one chat no longer disables Resume in another. Tests cover a double tap, a lost Resume answer re-enabling the button, the row's layout and live region, the conversation keying, a reasonless pause, and a paused queue's failed-send card and prompt wait. * perf(native-chat): the queue's pause reads the latest person's turn in O(1) The pause is derived on every publish, per subscriber, and each derivation copied and scanned every submission to find a person's accepted turn after the Stop. The reducer now keeps that fact as it folds rows: the submission row of the latest accepted turn whose origin is `client`. The Stop's and the restart's lift both read it directly. * fix(native-chat): a card handed off after a restart belongs to the process that sent it A draft's host_instance was only ever the process that first wrote it (or adopted it while waiting). A returned card from before a restart, sent again in this process and then withdrawn back to waiting, still carried the old process, so it raised a 'restarted' pause although no restart happened since it was sent. Every hand-off (the drain, Send on a card) now stamps the handing-off process on the draft in the consume's own update. * fix(native-chat): a queue pause shows only while Resume would send something After a Stop whose only remaining card was a returned one, or after a restart with only a card held by its own failed send, the queue published a pause with a Resume that could send nothing: a returned card waits for the user anyway, and a held one for its own Send. The pause is now published, recorded by a Stop, and kept only over a card it can hold back — waiting, with no hold of its own. The fact is retired in the same transaction as the write that removes the last such card, a hold or a refusal included. The publication's dedup also compared only the pause's reason, so a pause appearing or clearing with no readable reason could read as unchanged; it now compares presence first. * fix(mobile): the paused-queue header shows only while Resume would send a card The host publishes a pause while any card is waiting with no hold of its own, but it does not exclude cards behind a returned card, which the drain never sends. With only such cards, or only returned or failed ones, the header offered a Resume that would do nothing. The header now shows only when a waiting card with no hold of its own sits ahead of any returned card, as on desktop. * fix(native-chat): a queue pause counts only cards Resume would actually send A waiting card behind a returned one is blocked until the user acts on the returned card — the drain never sends past it — so a pause over only such cards still offered a Resume that sent nothing. The rule for "a card Resume would send" is now one function: waiting, no hold of its own, and not behind a returned card. The publication, a Stop's record and the fact's retirement all read it; retirement reads the rows in position order inside the same transaction as the write that took the last such card. * fix(native-chat): a returned card that blocks the paused cards hides the pause but keeps it The last change retired a Stop's pause as soon as a returned card blocked every paused card. Deleting that returned card then sent the cards behind it at once, with no Resume — not what the user asked for. The two rules are now separate. The pause is KEPT (recorded by a Stop, retired in the same transaction as the write that takes the last one) while any waiting card with no hold of its own exists, wherever it sits. It is PUBLISHED only while such a card is not behind a returned one, so the header never offers a Resume that sends nothing. Deleting the blocking card shows the pause again, and the cards behind it wait for Resume or the user's next turn. * fix(native-chat): a Stop pauses a card its withdrawal sent back even when that settlement was skipped The Stop checked the draft table for a card to pause. When the per-row hook that settles a withdrawn hand-off was skipped, that card was still 'dispatched', so the Stop recorded no pause; the drain later healed it back to waiting and sent it, although the user had pressed Stop. Retirement had the same blind spot and could drop a pause while such a card was owed. What a pause holds back is now one predicate, judged inside the transaction that records or retires it: a waiting card with no hold of its own (one SQL EXISTS), or a dispatched card whose consumed submission was rejected with a settlement back to waiting (read against the journal's submissions). The Stop first runs the owed settlement, as the drain does; if that fails, the owed card still counts, so the pause is recorded rather than skipped. recordPause now checks inside its own transaction and returns whether it recorded, and any draft-table write (and the per-row hook, the consume and the open-time repair) retires a pause that no longer holds anything back. * test(native-chat): pin the per-row hook's pause retirement; skip the judgement when no pause exists The retirement test recorded its second pause over a queue with nothing to hold back, so the recording returned false and the "retired" assertion proved nothing; ablating the per-row hook's retirement passed every test. The hold case now asserts the pause was recorded, and a new test has a delivered echo, through the per-row hook, withdraw the last card a recorded pause holds back. Retirement runs on every appended journal row, so it now checks the pause row by key first and judges nothing when no pause is recorded. Two comments were brought in line with the owed-hand-off rule and rewrapped. * test(native-chat): match main's append and dispatch shapes in the queue tests * fix(native-chat): read a compaction's settled submission through the send-result union * test(mobile): a queued card's Steer groups under the running turn With turn facts read from the turn record, a draft Steer hands over while a turn runs is scoped by the host to that turn, under a fresh submission id that names the card. On the phone it joins that turn's group, gets no bar of its own, and stays live with it. * feat(mobile): queued messages sit in one compact box, as on desktop Each queued message was its own tall card with a "Queued" header and a row of Steer / Edit / Delete text buttons, and the paused-queue line floated above them. The phone now draws the desktop's layout: one bordered box whose first row is the pause line with Resume (only while paused), then one compact row per message with a leading queue or alert icon, the text (up to two lines, since a phone has no hover title), a caption only when there is something to say, and Steer (Send for a returned or failed card), a trash button, and a "..." menu holding Edit message. The card model's label becomes a nullable caption with the desktop's rules (no caption for a plain wait or the queue's pause) plus a needsAttention flag for the alert icon. Touch targets stay 44pt inside their rows. * test(mobile): stub the queue's action sheet in the chat view tests * test(mobile): move the chat view's turn-status wiring tests to their own, type-checked file --------- Co-authored-by: Claude <noreply@anthropic.com>
Orca
中文 · 日本語 · 한국어 · Español · Français · Português
The AI Orchestrator for 100x builders.
Run Codex, ClaudeCode, OpenCode or Pi side-by-side — each in its own worktree, tracked in one place.
Download Orca
Features
Also in the box:
- Quick open — Search across worktrees, files, agents, commands, and repo context without leaving your flow.
- Account switcher & usage tracking — See Claude and Codex usage and rate-limit resets, and hot-swap accounts without re-logging in.
- Rich repo previews — Preview Markdown, images, PDFs, and repo docs in the workspace.
- Computer Use — Let agents operate desktop apps and visible UI when a workflow needs real interaction.
- Notifications and unread state — Know when an agent finishes or needs attention, then mark threads unread to come back later.
- And many, many more — we ship daily, so this list is perpetually behind. The changelog is the real feature list.
Supported Agents
Works with any CLI agent — if it runs in a terminal, it runs in Orca.
Claude Code
Codex
Grok
Cursor
GitHub Copilot
Muse
DeepSeek Harness
ZCode
OpenCode
MiMo Code
Amp
OpenClaude
Antigravity
Pi
oh-my-pi
Hermes Agent
Devin
Goose
Auggie
Autohand Code
Charm
Cline
CodeBuddy
Codebuff
Freebuff
Command Code
Continue
Droid
Kilocode
Kimi
Kiro
Mistral Vibe
Qwen Code
Rovo Dev
+ any CLI agent
Install
Desktop — macOS, Windows, Linux
- Download from onOrca.dev
- Or grab a build directly: macOS Apple Silicon · macOS Intel · Windows (.exe) · Linux AppImage · All builds
- Running
orca serveon a headless Linux server? See the headless Linux server guide.
Or via a package manager:
# macOS (Homebrew)
brew install --cask stablyai/orca/orca
# Arch Linux (AUR) — or stably-orca-git to build from source
yay -S stably-orca-bin
Mobile Companion — iOS, Android
Pair with your desktop app to monitor and steer your agents from your phone.
- iOS: Download on the App Store
- Android: Download APK 0.0.50 · Install guide
Community & Support
-
Discord: Join the community on Discord.
-
Twitter / X: Follow @orca_build for updates and announcements.
-
WeChat: Scan to join the Orca community WeChat group 10.
-
Feedback & Ideas: We ship fast. Missing something? Request a new feature.
-
Privacy: See the privacy & telemetry docs for what anonymous usage data Orca collects and how to opt out.
-
Show Support: Star this repo to follow along with our daily ships.
Developing
Want to contribute or run locally? See our CONTRIBUTING.md guide.
The relay that pairs the mobile app with a desktop host is also in this repository under
cloud/, with a separate pnpm workspace and setup guide.
Signed Builds
Windows code signing sponored/provided by SignPath.io, certificate by SignPath Foundation.
License
Orca is free and open source under the MIT License.










