Repository navigation
sync: upstream d2dbe2c385 into swaplabs/main - #29
Conversation
…blyai#23807) * perf(usage): persist a scan's analytics session IDs in one write Minting identities one at a time rewrote and fsynced the whole identity file per new session, so a first scan of N sessions did N durable writes and O(N^2) serialization inside the scan (~44s at 5,000 sessions). Batch lookups so each scan does at most one write. Fixes STA-8749 * refactor(usage): resolve IDs before loading snapshots; skip empty batches
…dinator (stablyai#22631) * feat(orchestration): deliver worker results to a structured chat coordinator Resolve a Run's handle-less session coordinator and a session:<id> mailbox to the live session, wake an evicted session for the delivery, redrive a session's own mail on its idle edge, route a terminal view's pointer through its PTY, and accept session:<id> (or a bare Orca session id) as a recipient. * test(orchestration): pin coordinator delivery through the real session host, wake, idempotence and session addresses * test(orchestration): type the coordinator mail fixture's attach params * fix(orchestration): refuse session recipients with the caller codes, and treat a worker without its identity as undeliverable * test(orchestration): pin a chat's terminal view reading the chat's coordinator mail * test(orchestration): read coordinator mail fixtures through checked guards instead of assertions * fix(orchestration): name a structured session's CLI by $ORCA_CLI_COMMAND in its pointer turn * fix(orchestration): render the pointer's CLI invocation for the shell the session runs in * fix(orchestration): address a session recipient where its check reads, so a structured worker gets its mail * test(orchestration): pin the runtime's own idle-edge redrive wiring; say a released session is not running, not ended * fix(orchestration): point mail that has not been pointed, not mail nobody has acked, naming the ack a held batch needs * feat(orchestration): hand a /clear-replaced chat's Runs and unread mail to the session that replaced it * feat(orchestration): adopt a /clear predecessor's Runs at the clear's commit, the edge its replacement's own status misses * fix(orchestration): log a wake that could not resume a session, instead of retaining silently * test(orchestration): give coordinator mail waits a budget that holds under a loaded parallel run * refactor(orchestration): narrow a retained pointer's dispatch state by type guard instead of a cast * fix(orchestration): give back a pointer whose admitted turn never ran A pending send stamped its rows delivered and dropped its operation row, so a provider that died before echoing left the last result pointed at nobody. The lane now awaits the admitted turn's settlement: accepted consumes the claim, anything else returns the rows and drops this send's operation row, so the re-point on the next edge is a new send rather than a replay of unknown. * fix(native-chat): keep a committed /clear from failing on its replacement observer The observer runs after the clear's durable commit; a throwing adoption turned a committed clear into a failed RPC. It is now best-effort and logged, like the status feed's observer, and the successor's idle edges re-derive the adoption. * test(orchestration): pin /clear adoption across a chain of clears and a predecessor's check A session cleared twice before any edge hands both predecessors' Runs and mail to the end of the chain. A predecessor still live in the clear's tail reads none of the re-addressed mail: a session's direct mailbox is consume-on-read and holds no replayable batch. * test(orchestration): type the pending-settlement host test's send input without an assertion * fix(orchestration): keep a chat's orchestration address across /clear by deriving its lineage A chat's orchestration address is now the first session of its /clear lineage. Every session of the lineage resolves to that one actor when it acts and when it is reached, and delivery goes to the lineage's live session. Nothing is rewritten at a clear, so the predecessor-adoption path is gone: the commit-edge observer, the idle-edge rebind, and the unread-mail re-address. That path could unbind the successor's own Run and orphan all but one Run of a chain. The idle edge now opens the orchestration database through its lazy getter and logs when it cannot, instead of reading a field that stays null until the first orchestration call after a restart. * fix(orchestration): give back a structured pointer claim an earlier process left open A pointer the host admits as pending stamps its batch delivered, and only an in-memory settlement waiter gives it back if no turn ran. A process that died in that window left the batch stamped with nothing to release it, so the last result on that mailbox was never pointed again. When the database opens, every surviving pointer operation row is from an earlier process: its stamped batch is found by the row's fingerprint, released, and the row dropped, before the restored-mailbox scan points it again. A row whose batch was never stamped keeps its id for the retry. * test(orchestration): pin that a cleared chat's sends carry its conversation's address * fix(orchestration): open the orchestration database at an idle edge only when it already exists A profile with no orchestration database has no mail to redrive, so a structured chat's idle edge no longer creates one, and says nothing. An existing database is still opened lazily there. * fix(orchestration): read a chat-coordinated Run's session through the actor's generation A handle-less Run names its coordinator session only by an actor that still counts at the Run's current generation, the same rule every other binding read uses; an actor an older binary's rebind or unbind left behind no longer routes the Run's mail. A test pins that a cleared chat's run-create and run-use write its conversation's root actor at the Run's current generation. * refactor(orchestration): address a session's conversation by its bare root Orca session id Carries the Orca session id rename into coordinator delivery. A session's orchestration identity holds its conversation's bare Orca session id, the /clear lineage root's, and its mail address is derived from it by formatOrcaSessionAddress; a Run a cleared chat creates or uses stores that bare root id. A session recipient carries the parsed id and its address as distinct types, a handle-less coordinator's session is read through currentRunCoordinatorOrcaSessionId, and session ids read from records or PTY bindings are checked with isOrcaSessionId before they become an identity. The session address prefix comes from the one exported constant. * refactor(orchestration): canonicalize a cleared session through the one id hook and the party resolver - canonicalOrcaSessionId now walks a session's /clear lineage to its root; the parallel session identity and lost-worker rule are deleted, so the caller resolver, recipient routing, reach and idle-edge mailboxes all resolve a session through resolveOrcaSessionParty. - A Dispatch row's assignee_orca_session_id goes through the same hook. - The terminal-view delivery lane is gone with the terminal handoff: no PTY is bound to a session, so a chat's mail is always a session turn. - Pins a send to a Run-less chat's session address after a restart, when the send must start the agent-session host before routing reads its record. * fix(orchestration): point a structured session with the PTY lane's exact text A chat or structured worker is now told what a terminal agent is told: the pointer is formatMessagePointer with the CLI name the PTY lane resolves for a local terminal (orca, or orca-dev in a dev build), with no shell-specific invocation and no ack lesson. The lane still excludes the batch a reader holds unacknowledged and points newer mail; that stays host-side, and the reader's own check replays the held batch and names its ack as it does for a terminal. * fix(orchestration): deliver a cleared structured worker's mail to its live successor A terminal keeps its handle across /clear; a structured worker's successor now does the same. Mail at the worker's handle, its dispatch mailbox, and a Run it coordinates resolved to the session minted for the worker, which /clear replaced. Each now walks the /clear lineage forward to the live session, which the caller resolver already treats as the worker. * test(orchestration): compare a chat's pointer turn to the PTY lane's text for this build's CLI name * refactor(orchestration): resolve the local CLI name once, for the PTY lane and the structured lane alike * fix(orchestration): let a /clear-ed chat restate its address, placed by the host's lineage The CLI entry compared a restated --from/--terminal with the injected session id as a plain string, so a cleared chat restating the address it had before the clear (its lineage root, the address it keeps) was refused. Only the host's session records know the lineage, so a session address that is not this session's own spelling is now sent as the caller param, where the host's canonical-id check accepts it or refuses it before any effect. Plain restatements are still dropped and any other name is still refused at the entry. * test(orchestration): read the coordinator journal through the async snapshot, and wait for the held turn's handover Main made journalSnapshot async and delivers an accepted send once the host hands it over, so the fixture awaits the snapshot and waits for the provider's turn before echoing it. * refactor(orchestration): point a chat whose agent is not running through the plain send Main's host no longer has hold/release: an accepted send starts the agent itself. The pointer lane's wake step called host.hold, which no longer exists, so it is deleted from the pointer host, the delivery lane and their tests. An idle or evicted chat gets its pointer through the same send a user message takes; the claim is still consumed only on accepted and given back otherwise. * fix(orchestration): leave a chat whose provider died stopped instead of respawning it for mail A pointer whose provider died before echoing it is given back. The death's own status edge then redrove the mail, and since a send starts the agent, a provider that died on every turn was restarted about once a second for as long as the mail was unread. The pointer lane now reads, on every attempt, whether the session's latest send never ran because its provider exited or could not start, and holds the mail until a later send runs. Every trigger passes through that gate: a parked retry, the idle edge's re-derive and new mail. A rejection for any other reason still points at the next idle edge. * fix(orchestration): hold mail after a start the person must fix, placing every failure kind A start refused for a reason only the person can fix (not signed in, history too large, a managed-account problem) or one the host stopped because it never came is held like a failed start: the mail waits for the person's next message, which also retries the start. An account switch still in progress is transient, so it stays ungated and the first edge after the switch settles points the mail. One exhaustive record places every rejection kind, so a new kind does not compile until it is placed. * fix(orchestration): retry a structured pointer under its own id instead of gating on the failure reason A pointer send that failed was given back and re-sent under a fresh operation id, so every status edge after a provider death was a new send that started the provider again. A reason-string gate held some of those deaths, but missed a Codex crash with turn/start in flight (it settles unknown with the connection's error), latched all later mail after one transient death, and did not keep a user's Stop. A retry now reuses the mailbox's operation id, which the host answers by replaying the recorded verdict without reaching the provider. The id is re-minted only for new mail, after a later send ran, for a row an earlier process left, or once an account switch settles. Rows are stamped only on accepted, so the admitted-stage claim, its give-back and the restart claim-release scan are gone. * test(orchestration): pin that a Stop keeps a pointer unsent before the next status edge, too * fix(orchestration): point a structured chat's mail by the same rule as a terminal's The chat lane pointed newer mail past a batch its reader had checked and not acknowledged, while the terminal lane skips a mailbox until that batch is acked. A chat now waits for the ack the same way a terminal does, and the lookup that let the chat lane filter the held batch out is removed. * fix(orchestration): refuse a session address that gate-list or task-list --run would drop The CLI entry lets a `session:` address that is not the session's own spelling through for the host to place, but gate-list and task-list send no caller when --run names the Run, so `--from session:<other>` was silently ignored. With --run they now refuse it (consumer_fenced) before any request, the same as a conflicting terminal handle. * fix(orchestration): keep a failed mail redrive from skipping a chat's first-turn workspace rename A structured session's status callback redrives its mail before the first-turn workspace auto-rename, outside any try, so a database error there threw past the rename. The redrive now logs its failure and returns. * chore: take main's pnpm-lock.yaml the merge of origin/main left stale * fix(orchestration): give a stamp or park decision its own variant so the send branch narrows * fix(orchestration): re-mint a held structured pointer from facts that cannot strand it A pointer row whose id had no submission in the journal was always resent under that id, before any re-mint test ran. After a rewind rebuilt the journal, or a send refused before it was recorded aged past the host's 24h admission window, the mail was held forever: neither the person's next turn nor a restart pointed it again. The re-mint tests now run first. "The agent has run since" is any accepted send submitted after the row was minted; "an earlier process minted it" is a row whose id this lane did not send, not a wall-clock comparison a clock step could fool; and a row the host never recorded is re-minted once it is too old for the host to admit. The account-switch exception is gone: nothing tells the lane when a switch ends, and each outside edge during one added another pointer and failure to the chat. A parked pointer now retains as turn-unsettled. * fix(orchestration): let the host check a session caller that gate-list or task-list --run names 5f753af refused any `--from session:<x>` beside --run that was not the session's own spelling, which also refused a /clear-ed chat restating its lineage root, an address the host accepts everywhere else. The CLI now sends that address with --run, and the host's declared caller check accepts the root and refuses anyone else (consumer_fenced) before any effect. * fix(orchestration): date a pointer on the journal's clock so a backward clock step cannot re-mint it every edge
stablyai#23907) Terminal daemons survive app updates, so new tabs keep spawning from the old v36 daemon and never get stablyai#23900's Codex shell function. Bump to v37 so new tabs move to a fresh daemon; v36 owners stay attachable.
…blyai#23667) * refactor(agent-hooks): one predicate for whether an agent's status hooks are on "Global switch on and this agent not turned off" was spelled out separately in the startup controls, the settings reconcile, the retained-home reconcile, the WSL preflight RPC, the CLI preflight and the OpenCode plugin selection. They now share one function, in a module light enough for the CLI's per-launch Codex preflight to load. The PTY spawn env derives the Codex flag from the switch and opt-out list it already carries, the same way it does for OpenCode and Pi, instead of receiving a second copy. * fix(codex): launch and resume prep honour Codex's per-agent hook opt-out Turning Codex off in the per-agent hook settings removes Orca's Codex hook entry, but launch prep and session resume read only the global hooks switch, so the next Codex launch or resume wrote the entry straight back into the real ~/.codex or the account's home. Both now read the per-agent predicate, which the PTY spawn env and startup already honoured. * fix(codex): turning Codex off per agent clears the real ~/.codex entry While the real-home lane owns ~/.codex/hooks.json, the legacy system-home sweep stands down. That gate read only the global switch, so turning Codex off per agent ran remove() with the sweep still suppressed and left Orca's entry in the real ~/.codex. The gate now reads the per-agent predicate, the same as turning every hook off. * test(codex): cover the system ~/.codex sweep gate for Codex turned off The gate that lets the legacy system-home sweep run was an inline closure in startup, so reverting it to the global switch left CI green. It is now a pure function beside the gate it feeds, with a table test and a remove() test on a seeded ~/.codex: turning Codex off strips Orca's entry and keeps user hooks; with Codex on the entry stays. * fix(cli): keep the agent-status hooks predicate loadable by the packaged CLI The CLI's prepare-codex handler imported the predicate from src/main, but the Electron build rebuilds out/main from its declared entries only, so the packaged `orca agent hooks` commands could not load it (package jobs and the CLI bundle-parity test were red). The predicate reads only settings, so it now lives in src/shared, which the CLI compiles itself.
…lyai#18750) Co-authored-by: Neil <neil@stably.ai>
…open (stablyai#23848) * fix(editor): follow system light and dark while the editor tab stays open The editor sampled the system color scheme once, when the tab opened. The rest of the window already listens for appearance changes, so the chrome updated and the editor surface did not until the tab was closed and reopened. Editor surfaces now use useDocumentDarkTheme, which re-renders on that change. An explicit light or dark setting still stays put. * refactor(theme): share one live dark-theme hook across every renderer surface Move useDocumentDarkTheme out of components/editor into hooks/ so non-editor code stops reaching into the editor folder for it. Convert the four surfaces that still read the system color scheme once at render (comment mermaid, both PR combined-diff viewers, automation prompt editor) so they follow OS light and dark changes like the editor now does. Replace the 145-line MonacoEditor mock test with a direct hook test covering system flips, explicit light/dark staying put, and unloaded settings. * test(theme): keep mounted-editor coverage and cover a converted surface Restore the MonacoEditor system-theme test so a mounted editor is still guarded against ceasing to use the hook, and add one for CommentMermaidBlock as a representative newly converted surface. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…is a message the chat sends (stablyai#23059) * 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. * fix(native-chat): the conversation outlives its agent Opening a chat no longer starts its agent. A conversation is reached through one host accessor that opens its journal at rest, and a send is what starts the agent, through the delivery loop. One idle sweep, every five minutes, stops an agent that has been quiet for thirty minutes and owes no work, then drops an open journal handle that is only a cache. Its record, tab, status row and readers stay. - hold and release are no-ops; hold still builds the host for shipped mobile builds. - The holders, the holds, the release clock and the exit respawn are deleted. - Options, the model list, the goal and the context meter answer at rest; a model pick at rest is recorded as intent for the next start. - Compact, rewind, clear and goal changes start the agent first. A send does too when a rewind is still in doubt after the conversation opens. - Orchestration routes mail and group addresses on ownership (the record plus the chat tab), not on whether the process runs. An open dispatch keeps its worker running. - The restart continuation is a send; Resume all holds each slot until the message is handed over or rejected. - A read error never replaces a loaded transcript, and shows the host's own words. * 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. * fix(native-chat): a request that failed reads as failed A structured chat whose only message the agent's start refused read as a green finish, and a cancelled structured turn did too: the host published a verdict only for turn records, and structured rows carried no `interrupted`. The host projection now reads the session's latest request: its turn's outcome, or `failure` for a send the agent or its start refused. A send that was withdrawn, or left undelivered by a restart or a close, fails nobody and makes nothing listable. The ingest publishes `interrupted` as the hook lanes do, and every reader decodes the verdict through one accessor, so a failure reads Failed on the dot, the rollups, history and `worktree ps`, behaves like a cancellation in every clean-finish policy, and notifies as "failed". * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): a verdict change republishes the mobile status projection * refactor(native-chat): the store's retention trigger keeps its flag compare A verdict change always moves the completion clock the same check already reads, so a second verdict compare there caught nothing new. * test(native-chat): a user message the provider journaled keeps its session listed * 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 * fix(native-chat): a restart offer ends when the chat's agent starts again The offer used to end only when the chat's newest user message changed, because opening a chat started its agent and that start could not be told apart from real activity. Opening a chat starts nothing now, so the host reads the fact it already publishes: a chat's status row goes from not host-owned to host-owned exactly when its agent is started. At that edge the offer and any failure record for the chat are withdrawn, unless the start is a resume action's own (its continuation is the oldest undelivered message). A continuation and a message racing to be first are decided at acceptance: the continuation is refused, quietly and with nothing filed, when any other message was accepted since the restart. A failed continuation start leaves the offer retryable, and each resume action sends its own message id. Deleted: the newest-user-message comparison, its journal reader, the continuation filter, and the failure ledger's own "answered by the chat" check. The marker still carries its message id for one release, so the previous build can read it. * fix(runtime): end a transcript stream when its client unsubscribes Desktop: the IPC subscription controller was dropped as soon as the streaming handler returned, which for most streams is right after it binds. A later runtime:unsubscribe then found nothing to abort, so the host kept the subscriber and derived and sent every publish to a channel no one listened to. The controller now lives until the renderer unsubscribes, resubscribes the same id, or goes away. Mobile: disposing an agentSession.subscribe stream now sends agentSession.unsubscribe with the stream's frame id, so the host ends that subscriber and leaves a sibling stream on the same socket running. The direct path now passes the frame id the relay path already passed. * fix(native-chat): a late provider-session update keeps a failed recovery record failed A provider-session heartbeat that rewrites a completed recovery record kept its interrupted flag but dropped the outcome it was copied with, so a live failed checkpoint read as a clean finish until the next status write. * 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`. * test(native-chat): the terminal-bell check asserts the renamed verdict field The bell notification test still checked for agentInterrupted, which no longer exists, so it could not catch a verdict leaking into a bell dispatch. * fix(native-chat): a failed turn ranks like a completion for attention Attention readers (completion time, Smart Sort, sticky retention, Cmd+J Recent) now demote only a turn the user stopped. A failure is news the user has not seen, so it keeps its completion time, ranks in the Done class, stays retained after its pane goes away, and a retained failure reads failed in the worktree rollup instead of done. Clean-finish policy (hibernation, pane ownership, the value moment) still treats a failure like a stop. The retention trigger compares verdicts again: success -> failure no longer moves the completion clock. * fix(native-chat): one fact ends a restart offer: the chat moved on since the restart The offer is live while no other message has been accepted in the chat since the restart and its agent has not proved a start since. The offer list, the resume's reservation check and the continuation's acceptance check all read that one fact, so a message whose start then failed withdraws the offer too, and a stale click finds nothing to act on. The fact is read off the conversation's open handle, which the restart closed, so it is retired durably whenever it may have changed: a message accepted, a start proven. A close and reopen within the same run therefore cannot bring the offer back. A continuation rejected before it reached the agent does not count, so a retry after a failed start still runs. Deleted: the quit-time gate on withdrawal, which changed nothing because the withdrawal and the quit's own offer write share one queue; the per-action "withdrawn" flag and the separate acceptance check it paired with. * test(native-chat): an older build reads the restart offer this build records The offer lives in a file the previous release reads after a downgrade. Pin that against the pinned release's own capsule, and run the lane when the marker or the capsule changes. * fix(native-chat): read a restart offer against where the journal stood when it was taken "Since the restart" was read off the conversation's open handle, which the idle sweep closes: after a reopen, a message the user had already sent looked older than the handle and the withdrawn offer came back. The offer now records the journal position (epoch and sequence) at the moment it is taken, and a message accepted after that position, or a journal on another epoch, means the chat moved on. That is derived from the journal, so it holds across any number of closes and reopens. An older build's offer has no position; only a start withdraws it. Because the message half is now durable, the offer is no longer rewritten in the recovery file on every accepted message; a proven start still writes it, since only the host that saw the start knows of it. * test(native-chat): wait for the listing's retire write before reading the recovery file * refactor(native-chat): every journal row states which turn it belongs to Rows gain a turn scope stated by the write that creates them: the open root turn, or the conversation. A queued message takes its scope from its handover. Rows stored before scopes existed are placed on replay by the root turn open when they were created, so no persisted state is needed for them. Rewind keeps each retained row's scope and producer, so a subagent's row stays its own. * fix(native-chat): keep the terminal-backed chat's read error over its local echoes Messages winning over a read error is right for the structured chat, whose read retries and whose messages came from the transcript. The terminal-backed view assembles its list from local echoes too (a launch prompt, a pending send), so a failed read there showed only those bubbles and no error. Only the structured pane now keeps messages over an error. * fix(native-chat): a start retries the exit settlement a failed journal write left owed An agent exit whose journal settlement write failed releases the lease latched until a retry lands. Reopening the chat used to be that retry; with reveal now only opening the journal, nothing retried it before the next app launch, and every send was refused. The start the send needs now runs the retry first, where the attach would. * fix(native-chat): a failed main agent reads failed while its subagents still work The verdict is now read from the main agent's own state, not the folded row: a main agent that is done and failed has a verdict even while its subagents keep the row working. Without mainAgent (history, worktree ps, older hosts) the old combined-done rule stands. Display marks the verdict through agentVerdictDisplayMark: a failure outranks every combined state on the agent's dot, label, tab badge, dashboard and activity rows; a stop marks only a done row, so a successful or stopped main agent with live subagents still reads working. Subagent rows keep their own state. The worktree card, terminal tab and Cmd+J rollups share one pane fold and rank a pending question, then failed, then working, monitoring, interrupted and done. worktree ps publishes the main agent's outcome on a working row, and the mobile mirror reads it. The store's change check, the paired-client mirror's equality and its epoch now see a verdict change on a working row, which otherwise moves no state or clock and left the worktree card reading working. Clean-finish policy is unchanged: a working row is never hibernated and has no completion time. * perf(native-chat): answer the owner check without opening the chat Worktree activation calls agentSession.handoffStatus for every chat tab in the worktree, and the answer comes from the session record alone. Reaching it through the accessor opened each resting chat's journal (a full read, the crash-boundary write and a restored status publish), then kept it open for the idle window. It now checks the record and the adapter's support, as before this series, and opens nothing. * fix(native-chat): a read waiting on the session lock opens nothing once quit began The accessor checked for quit before queueing the open, so a read queued behind a session task ran its open after teardown had begun and indexed a journal no teardown step would close. The check now runs at the open itself. * test(native-chat): pin stated turn scopes, the upcast of unscoped rows, and rewind attribution * fix(native-chat): /compact is a message the chat sends, run as a turn of its own The conversation command RPC now accepts /compact into the queue like any send and answers once it is handed over. The delivery loop opens the command's own turn, starts the provider on it, and waits for the provider's end off the session's queue, so messages typed meanwhile are held and delivered after it, even when it fails. It settles by re-reading the journal: a child that died meanwhile already wrote the verdict. Stop ends the command at once. The 180 s completion window, the unconfirmed row and the recovery of an older build's compaction record are gone; that record no longer gates anything. On Codex the provider turn the command opens is claimed into the command's turn. * fix(native-chat): read a failed resume's chat before calling it retryable Whether a failed resume is retryable is the offer's own rule: the chat has not moved on since the restart, read from its journal. The failure list read it only for a chat already open, so once the idle sweep closed a chat the user had moved on in, its failure showed Retry again, and the click did nothing. The list now opens the failed chats first, as the offer list does. * test(native-chat): type the provider event sink the settlement test reaches for * docs(native-chat): the worktree ps outcome comment no longer claims old hosts send it The field is new: an old host sends no outcome at all, so a reader falls back to interrupted. The removed clause said old hosts send it on done rows, which never shipped. * fix(native-chat): say the structured read keeps trying only where it does The structured pane's "Orca keeps trying to load it" line never showed: the view state filled in an untranslated fallback whenever the read error had no text, and the empty state prefers any message. The view state now leaves the message out, so the structured pane shows that line and the terminal-backed pane its own translated one. Mobile's structured lane does not resubscribe after an error frame, so it no longer makes the claim. * fix(native-chat): rows group under the turn their record names, not the one above them Each row's turn is the turn its stated scope names, anchored on the entry that opened it, or on the turn itself when the provider opened it unasked. So /compact groups its own rows and the previous turn is untouched, a message typed into a running turn joins it, and a provider-resumed turn folds under its own Worked-for. A row reporting how a turn ended, an error or the compaction separator, never folds. Desktop and mobile read the same keys; a host that states no scope keeps today's positional grouping. * test(native-chat): await the send's settlement instead of polling for the start The at-rest send tests polled for the provider start with vi.waitFor's one-second default, which a loaded machine outran. They now await the host's own settlement of the message. * docs(native-chat): the status-store listing rule names provider-journaled user messages * fix(native-chat): a restart offer resumes any time after the quit, and knows its own continuations The continuation's message id was dated by the quit, and the ledger refuses a new id dated more than a day back, so Resume or Retry a day after quitting was always refused (on main too). It is now dated by the resume action. Telling a rejected continuation from the user's own message read the operation ledger, whose rows expire after about a day; after that a failed resume stopped being retryable. The offer now records the continuation each action sends on its own capsule entry, bounded to the newest 16, so the ids end with the offer. The ledger read is deleted. * fix(native-chat): a /compact is not a request the sidebar, notifications or restart resume report The sidebar's prompt, preview, verdict and instant, the turn-completion feed, and the restart-resume marker read past a conversation command and its turn to the last real request, so a /compact neither notifies nor re-dates the row, and a command in flight is never offered as work to resume. An older client shown a command's turn in the legacy form names the session's own agent. * fix(orchestration): route no mail to a structured worker its orchestration released A structured worker is routed on ownership, and a resting worker's lease is released, so ownership held while its chat tab stayed listed. A worker the coordinator abandoned and then released, found at rest by the release, therefore still took peer mail and @worktree: broadcasts, and each one restarted its agent. Routing now also reads the orchestration's own resource row: once it is released, direct mail, group addressing and worker-show's addressable answer drop the worker, as they would a terminal worker whose terminal closed. The chat tab stays, and nothing new is stored. * fix(native-chat): a failed retry names the user's prompt, not Orca's continuation A resume's continuation is written to the chat before its start, so after a failed attempt the chat's newest user message is that rejected continuation. A second failure then showed Orca's own restart text as the chat's prompt. A retry now keeps the prompt its first failure named. * test(native-chat): pin what a conversation command's admission refuses at rest and at handover * test(native-chat): tests merged from the base state which turn their rows belong to * fix(native-chat): a refused send notifies failed through the completion feed The host's completion feed followed only the newest turn, so a send the agent or its start refused, which creates no turn, read Failed on its row but sent no notification. The feed now follows the session's latest request, read from the projection the status feed already makes for the commit: a turn keeps its id, a refused send is named by its journal item key. It announces only while the session is idle, as the row reports a verdict, so queued sends refused one commit at a time notify once, and a withdrawn send falls back to a request already announced. * fix(orchestration): read the released row optionally, as the authority does worker-show's observation called the row lookup directly, which a runtime double without it threw on and failed the structured tab-retirement release. * chore(native-chat): one import per module and no unexplained casts in the turn-scope changes * test(claude): pin which turn a Claude row joins, including a subagent's after the turn ends * fix(native-chat): the status bar drops a restart offer the chat moved on from The renderer re-read the host's restart offer only when a failed chat showed activity, so after a message withdrew a pending offer the host answered no chats while the status bar kept counting one, and clicking it opened nothing. The same watch now covers pending offers: a status change in an offered chat asks the host again, once. * fix(native-chat): a refused steer is read from the turn its handover named The latest-request reader decided whether a refused send had joined a running turn by comparing host clocks: its handover time against the previous turn's end. The handover row now states the turn it delivered into, so the reader reads that instead and the clock comparison goes. A journal written before handover rows stated a turn is scoped on replay from the turn open when each row was written, which can differ from the clock reading only when a send and a turn's end share a millisecond. * fix(mobile): the native-chat controller contract carries the turn journal The controller and overlay already pass nativeChatTurnJournal, but the contract type never declared it, so mobile failed to typecheck. * fix(native-chat): the live turn is the running turn, not the newest user row A turn the provider opened on its own (a background wake, a resumed turn) anchors on its own record, but the list still treated the newest user row as the live turn. While such a turn ran, the settled user turn before it lost its duration and the running turn's own rows were drawn as settled, so its tool calls lost their live state. nativeChatTurnMembership now answers both questions from the turn record: each row's turn, and the live turn (the running root turn's anchor, else the newest user row, which is also all an unscoped host has). Desktop and mobile key liveness, the timing clock and the live status's row on it. * test(native-chat): a turn the provider opened keeps its own clock Pins that the local turn clock follows the live turn, so a wake after a settled turn does not restart that turn's clock when no host durations are recorded. * fix(native-chat): a running turn no message opened draws its status on no row Its live status belongs to the transcript-tail indicator alone. Once it settles, its duration draws at its first row as before; a running turn a message opened still draws on that message. * fix(native-chat): every copy of a row carries the main agent's own status History entries, sleep records and `worktree ps` rows carried a flattened top-level `outcome`, copied under different gates and without the main agent's clock. They now carry `mainAgent` (state, outcome, stateStartedAt), the type the live row already persists and sends, and every copy site takes it with `interrupted` through one function, `agentVerdictFields`. - The accessor reads `mainAgent` then the legacy flag; the mobile mirror matches it line for line. - Sleep records admit `mainAgent` with `normalizeMainAgentStatusField`, so a malformed value drops the field, never the record. - Mobile dates a main agent that failed under live subagents by its own clock, as desktop does, and its row equality compares `mainAgent`. - The activity feed reads a history entry's own `mainAgent` instead of rebuilding one; the sync key and history equality compare it. * test(native-chat): pin the worktree ps verdict across host and phone versions Pairs the real v1.4.212 host and phone row reader with this build: an old phone reads a new host's rows by `interrupted`, a new phone reads an old host's rows (no `mainAgent`) the same way, and a new phone reads a failure under live subagents as Failed, dated by `mainAgent.stateStartedAt`. The release checkout now carries the phone's self-contained row reader, and the lane runs when the `worktree ps` row producers change. * test(mobile): name the parity table's row for its role * test(native-chat): a roster of idle or finished children does not keep an agent awake The sweep reads owed background work through the shared child-work liveness that upstream's release clock adopted; a child that went idle or finished is not work the agent still owes. * fix(native-chat): a request that settles while the user is asked something notifies once The completion edge waited for an idle session, and a pending prompt (including a subagent's approval) is not idle. Structured chat has no other attention producer, so a main turn that finished while a subagent waited on the user sent nothing until the prompt was answered. The edge now waits only on owed work (a running turn or an unanswered send), which the projection reports even beneath a pending prompt. A request that settles with a prompt pending announces once; the renderer words it "needs input" from the host status mirror's `attention`, and answering the prompt keeps the same request identity, so it does not announce again. The wire shape is unchanged. * fix(orchestration): a task dispatched into a resting structured worker keeps it running The sweep's open-dispatch check read only the worker-start dispatch that owns the worker's terminal resource, so a task later dispatched to the same worker (orchestration dispatch --to, which writes a dispatch with no worker row) did not count: after thirty quiet minutes the worker was stopped while that task was open, and its coordinator read exited. Any unsettled dispatch addressed to the worker's process incarnation now counts, derived from the existing rows. * fix(native-chat): a command's wait ends when its child does The delivery loop waited for a /compact only on the adapter's compaction tracker, which learns of the child's end only on some exit paths: a Codex exit or close, and a Claude close, never reach it. The wait then never ended, so nothing queued behind the command was delivered again, Stop had no child to answer through, and the tracker's leftover entry refused the next /compact. Every way a child ends passes endProviderChild, so the host now offers a per-child end signal there. The loop races the tracker against it (the dead-generation settlement has already written the command's verdict), and on that end asks every adapter to release the command, so a later command runs and no later provider turn is claimed into the dead one. The adapters' own exit-time releases were unreachable (Codex) or covered one path of several (Claude), and are removed. The Codex RPC test harness moves to its own module so the exit can be driven through the real adapter's connection callback. * fix(native-chat): keep refusing sends during a command on an older host An older host's controller still refuses a send while a conversation command runs, so dropping the client's block turned every message typed during /compact into a 'not sent' row with Retry there. The block stays for hosts that do not run the command as a send-path turn, and goes only for those that do. The signal is one the client already holds: a host that runs /compact on the send path states a turn scope on every journal row it writes, the same fact turn membership uses to tell it from an older host. Both now read it from one predicate. On an empty conversation, or one whose rows all predate the upgrade, the signal is absent until the command's own entry streams in, so that brief window keeps the old local refusal; no capability or wire field is added. * docs(native-chat): comments stop describing the hold this PR removed Eight comments still justified orderings and teardown choices by a viewer or dispatch hold that pinned the provider child. Nothing holds any more; the orderings stand for the binding's redrive subscription and parked mail, and a chat's agent runs from a send until the idle sweep rests it. Comment-only. * fix(native-chat): the completion says when the user is being asked A request that settles while a prompt waits on the user was worded "needs input" from the renderer's status-feed mirror. Remote clients receive the status and completion streams over separate sockets, so they can arrive in either order and the wording could be wrong both ways. The host already knows at emit time, so the completion now carries an optional `awaitingUser: true` in that case and omits it otherwise. The renderer words the notification from that field alone and no longer reads the status mirror. Old clients ignore the field and word by outcome; old hosts never send it. * fix(native-chat): a restart offer keeps the start its own continuation made Whose start ended an offer was decided at read time, from whether the offer's continuation was still the queued message. Once the provider refused that continuation, the child it had started read as someone else's start, so the offer ended and its failure showed no Retry. The delivery loop now records which queued message a start is for on the in-memory child, and the child's end carries it; the offer counts a start as its own when that message is one of its continuations. * fix(native-chat): a rewound turn still names the message that opened it A Codex rewind rebuilds the epoch without submissions, so each sent message survives only under its provider key. The kept turn records still named the submission key, so each turn anchored on itself and its rows grouped apart from the message that opened it. The rewind now renames the turn's opener along with the message. * fix(native-chat): Stop ends only the command it names Stop on a command turn abandoned whatever compaction the session had pending, so a late Stop for an earlier /compact cancelled the one running now. The tracker now ends a command only when the Stop names its turn, and the cancel reply reports whether it did. * fix(native-chat): an agent gets a full idle window after its owed work ends The sweep measured quiet only from the last journal row, so once a subagent, command, monitor or dispatch that had outlived the window ended, the agent was stopped at the next tick. A child can read done before the lead's wake-up turn writes anything, and stopping in that gap loses the wake-up. The sweep now counts owed work it observes as activity, which gives the agent the full window afterwards, as the release clock it replaced did. * test(claude): the options-read fixture runs a live child The fixture marked its conversation running with a hasProviderChild field the session type does not have, so the read took the at-rest path and refused a session with no record. It now carries a child, which is what the read checks. * test(native-chat): host tests reach its collaborators through a typed seam The rest-test rig and three test files read the host's private members with Reflect.get and cast the result. The host now exposes one test-only accessor, collaboratorsForTests(), and the subscribers class a subscriberCountForTests() beside its existing retainedActivityCountForTests(), so the tests are checked against the real types and the casts are gone. * fix(worktree-status): a departed agent's failure yields to live work on the worktree card A retained failed agent has no expiry, so ranking it with a live failure pinned the card to Failed over other panes' live work. It now ranks below working, monitoring and permission, and above every finished outcome. * refactor(orchestration): one owner answers a structured worker's custody Routing, group addressing, worker-show and the idle sweep each composed their own reading of whether orchestration still holds a structured worker, so each new obligation or retirement state had to be added to every reader. structured-worker-custody now derives both answers from the worker-terminal list state coordinators see in worker-list: addressable is owned and not released, and owed work is an active custody or an unsettled task dispatched to the same incarnation. The owner's state is read through the remote dispatch attachment too, as the terminal transfer lookup already does. Behaviour is unchanged; a settled worker awaiting its coordinator still rests. * refactor(orchestration): owed work is an open dispatch on the worker's incarnation A supervised worker's own dispatch context stays open exactly while the worker is active, so the separate active-custody branch only repeated it. Owed work is now one fact, which also states the policy that a worker awaiting its coordinator's decision may rest, and both custody decisions are written once at the top of the module. * docs(agent-status): a departed agent's failure ranks below live work on the worktree card * fix(native-chat): a restart offer knows its continuations by a tag in their id The offer recorded each continuation id in a list on its capsule entry, capped at 16, and a running action's id in memory. Both could disagree with the journal: past the cap an old rejected continuation read as the chat moving on, and a crash during a retry restored the failure's older entry, which lacked the retry's id. Each continuation id now carries a tag derived from the offer (its teardown and chat), then the action's own part, so any continuation of this offer, queued or rejected, is recognised from the journal row and the marker alone. The persisted list, its cap and the in-memory action map are deleted; the agent-start withdrawal keeps an offer whose own continuation the start was for, read against the stored marker. * test(runtime): the legacy-worker reveal test judges its stale snapshot inside the wait The tui-idle probe reads through readTerminal, which now awaits the structured worker check before the PTY read, so the probe's snapshot request starts a microtask later. vi.waitFor missed it on its first check and polled again at 50 ms, the same moment the wait's own 50 ms timeout fired. The stale snapshot then resolved after the wait had already timed out, so the test passed without judging it, and the rejection landed before any handler was attached. Vitest reported that as an unhandled error and failed the shard. Polling every 1 ms sees the request within a few ms, so the snapshot is judged while the wait is still pending. * fix(native-chat): a message held behind /compact is drawn where it was handed over A message typed while /compact runs was drawn above the compaction's result, between itself and its own answer. The reducer kept every item at the sequence and timestamp of the row that created it, and a queued message is created at acceptance, long before the command it waits behind writes its result. The phone orders by that sequence and the desktop by that timestamp, so both put the message first. A queued message now takes its position from its handover row, the same row that already states its turn scope. Everything the agent did before the handover, a command it waited behind included, draws above it. This holds for every held message, not only /compact's, and needs no client change: every client, older builds included, reads the position the host publishes. A live batch already carries the item when its dispatch row lands, and history pages cut the reduced timeline by sequence, so paging stays contiguous. * fix(native-chat): a phone's send during /compact answers without waiting out the compaction A client that predates accepted-send replies, which is every phone build, has its send reply held until the host hands the message over. A message sent during /compact is not handed over until the compaction ends, so the phone's 15 s request timeout fired first and showed the message as unconfirmed. That wait now also ends once the message is queued behind a running command. This is read from the journal's running turn and needs no new state. Every other wait still ends at the handover: behind a starting child or an ordinary turn, and for restart resume, the command front door and orchestration, which keep the plain handover point. * perf(native-chat): a rewind places provider items with one pass over the merged rows A Codex rewind gives each provider item the old epoch never held the turn record for its provider turn. It found that record by scanning every merged row, restoring each row's body, once per provider item. That is quadratic, and it runs on the host's main thread up to the journal's 10,000-row cap, twice per rewind. A rewind record written before rows carried their scope holds no scope for any provider item, so it paid the full cost. The merge now indexes turn records by provider turn id once, keeping the first match as the scan did, and each provider item looks its record up. * 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. * fix(native-chat): a message waiting behind /compact is drawn after it until it is sent A message sent while /compact runs is placed where it was handed over. It was still drawn where it was accepted until then. /compact writes its result one step before the handover, so for that step the waiting message sat above the compaction's separator. A message the host accepted but has not handed over is not part of the conversation yet, so both clients now draw it after everything the agent has done. The shared projection moves it to the end, which is the order the phone draws. The desktop ranks it with the other not-yet-sent rows, after the streaming preview. At handover it takes its place from its handover row, which is also after the separator, so it never appears above the compaction it waited for. * fix(native-chat): the idle sweep reads owed work every tick Owed work counted as activity, but the sweep read it only once the idle window had elapsed, so it refreshed the clock at most once a window. Work that ended just before the next read left the agent to be stopped at that read, moments after the work ended, which is the gap the refresh was meant to cover. The sweep now reads owed work on every tick for a started agent, so the window always runs from the last tick that saw work owed. * fix(native-chat): a continuation handed to the agent stays sent The offer read its own continuation as not reaching the agent while its dispatch was pending, which also covered one already handed over and still unanswered. When the wait for that answer ended first, the failure it filed read as retryable, and a retry sent a second continuation to an agent that may have acted on the first. Only a continuation still queued, or rejected, is now read as unsent. * 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(cross-version): load the phone row readers without mobile's toolchain Vite transforms a file against its nearest tsconfig, and mobile/tsconfig.json extends expo/tsconfig.base.json, which the root-only cross-version lane never installs. The worktree ps verdict suite imported the current phone row reader from mobile/ directly, so CI failed with TSConfckParseError before any test ran. The harness now imports a copy of the working-tree reader placed under the checkout cache, where the root tsconfig applies, as it already does for the release checkout's copy. Both readers are still the real files. * test(cross-version): keep the checkout path-guard message and justify the copy import's cast * fix(native-chat): a command ends only by its own provider answer or its child's end Stop no longer settles a conversation command. It interrupts it like any turn, and when the provider cannot take that (Codex has not opened the command's turn yet, or Claude refuses the interrupt) it stops the child, whose dead-generation settlement writes the verdict. The pending command now lives on the provider child's own session instead of an adapter-wide map keyed by session, so it dies with the child and nothing has to release it. Claude's /compact is sent under a uuid the slot records, and only a root result naming that input (or naming none) ends it; its outcome is read with the ordinary result reading, so a stopped /compact is a cancellation. * fix(native-chat): a command's settle answers its message before ending its turn The two writes are not one batch. Writing the message's answer first means a crash between them leaves a running command turn, which the stale-turn sweep already settles, instead of an ended turn whose message reads as in flight forever. The settle now writes only while the command turn is still running. * fix(native-chat): "Worked for" counts from the handover, not the send A message held behind /compact, or behind a cold start, used to count the wait as the agent's work, although its row is drawn at the handover. Every handed-over submission's turn, the command's own included, now starts at the handover row's instant, falling back to the send time for a host that recorded none. * test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * test(native-chat): the interrupted create's own retry continues again The merge of main's lease-latch fix replaced that test's retry of the interrupted create, under its own operation id, with a fresh start whose result nothing read. That fresh start passes with the released-reservation continuation deleted, so the case the fix exists for went untested. The retry and its assertion are main's again. * docs(native-chat): three comments that still had views starting agents A start with nothing queued now comes from a command, goal change or rewind; an interrupted compaction left alone would refuse every send, so no agent would ever start to finish it; and a current host raises the unattached read refusal only once quit began, with the attach window belonging to an older host. * 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): a s…
…s own cancelled event (stablyai#23862) * fix(claude): settle a queued send the CLI withdrew from its own cancelled frame Claude reports each uuid-stamped command's lifecycle (queued, started, completed, cancelled). A send it withdraws from its queue gets `cancelled` before the interrupt or cancel_async_message answer, so a lost or failed answer no longer leaves that send pending: it settles as withdrawn, with the same reason and words as the receipt path. A command the CLI already started also ends `cancelled` when its turn is interrupted or fails, so `cancelled` after `started` is not a withdrawal; an echoed send has left the waiter lists and is never reached. Tests replay real 2.1.280 captures, scrubbed. * fix(claude): release a doubted send when the CLI reports its session idle A Claude send whose write ended in doubt is recorded `unknown`, and a live `unknown` reads as work still owed, so the chat showed Working until the child exited. Claude sends `session_state_changed idle` only once its whole queue has drained, so it can no longer be holding that send. The runtime now routes that report to the host's existing release, the same one Codex's thread-stopped report uses; it retires `unknown` only, never `pending`. * fix(claude): keep a command's started mark when a redelivery re-emits queued; fixtures name msg_lifecycle_v1
…g it in the chat (stablyai#23893) * fix(codex): log a late reply to a timed-out request instead of showing it in the chat * fix(codex): say a reply had no waiting request rather than an unknown id
…ords (stablyai#23683) * fix(terminal): a confirmed workspace removal prunes that workspace's close records * refactor(terminal): a confirmed removal passes its host once, with or without one Both branches now made the same call; the split only existed to keep the old two-argument arity. * refactor(terminal): removal callers state why a workspace is going, and the store derives what goes with it Replaces the pruneCloseRecords boolean with a required cause: removed, forgotten or unlisted. Only the scan path is unlisted, and it keeps close records; every other cause drops them. Also pins that removing one host's copy of a same-id worktree leaves the other host's records. * refactor(terminal): a runtime orphan forget states it forgot, not that the workspace is gone The runtime removal wrapper hard-coded 'removed' for every caller, including the orphan path that leaves the directory and its git registration in place. Callers now pass their cause, as the desktop wrapper's callers already do, and the orphan path says 'forgotten' like the desktop Forget it mirrors. Pruning is unchanged: both causes drop the close records. * test(terminal): the runtime removal specs reach the wrapper through one typed helper Adding the cause argument touched seven calls that each cast their fixture store; one helper now carries the single justified cast. * test(terminal): the close-record removal spec states that Forget takes the records too * refactor(terminal): removing a workspace's session rows always takes its close records Every caller of the owner-removal chokepoint is an explicit removal, and the only host-driven one runs after a successful git scan, so the removal cause threaded through the removal paths decided nothing. Drop it, prune unconditionally, and seed the deregistered-repo sweep from close records so a project moved to another profile does not leave them behind. * refactor(terminal): the owner scan drops close records beside the other worktree-named records The standalone pruner and its shared helper only existed to be skipped for one removal cause; with the cause gone they are a second scan beside the one that already walks every record naming a worktree.
…tablyai#23778) * feat(native-chat): resuming interrupted chats runs in the status bar Pressing Resume in the restart dialog now closes it and shows a "Resuming N chats" spinner in the status bar until the host answers, the way a skill update does. The in-flight state moves from the dialog into the shared offer store so it outlives the dialog. * fix(native-chat): keep a resume dialog the user reopened mid-run open A footer Resume closes the dialog at once and the status bar carries the run. Its settle step still closed any dialog open when the host answered, so a dialog the user reopened mid-run vanished under them even when a chat they had left out was still offered. The handed-off resume no longer closes the dialog when it settles. A confirmed host answer with nothing left now retires the open request in the store, so the request cannot stay latched over an empty offer. A failed read still only hides rows and keeps the request. * fix(native-chat): a resume settling never closes the dialog A row Retry that cleared the last failure closed the whole dialog, even while other chats were still offered (a row Dismiss left it open), and even when the user had closed and reopened it mid-retry. The dialog's open request now changes only through the user's own ways out (Close, footer Resume, Dismiss all, Open chat) or the store's retirement on a confirmed empty host answer. * docs(native-chat): the resume dialog comment no longer says the skill dialog closes on update * fix(native-chat): each opening of the resume dialog starts from the default ticks The dialog never unmounts, so a chat unticked before Resume stayed unticked when the dialog was reopened, and after the run it read as a disabled "Resume 0 chats" over the one chat left. * fix(native-chat): a dialog reopened mid-resume ticks only the chats being resumed Each opening starts from the rows' default ticks, so a dialog reopened from the status bar mid-run showed a chat the user had left out of the resume as ticked. While a resume is in flight the rows now show the store's in-flight set; once it ends they show this opening's own ticks again.
…it (stablyai#23679) * 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. * fix(native-chat): the conversation outlives its agent Opening a chat no longer starts its agent. A conversation is reached through one host accessor that opens its journal at rest, and a send is what starts the agent, through the delivery loop. One idle sweep, every five minutes, stops an agent that has been quiet for thirty minutes and owes no work, then drops an open journal handle that is only a cache. Its record, tab, status row and readers stay. - hold and release are no-ops; hold still builds the host for shipped mobile builds. - The holders, the holds, the release clock and the exit respawn are deleted. - Options, the model list, the goal and the context meter answer at rest; a model pick at rest is recorded as intent for the next start. - Compact, rewind, clear and goal changes start the agent first. A send does too when a rewind is still in doubt after the conversation opens. - Orchestration routes mail and group addresses on ownership (the record plus the chat tab), not on whether the process runs. An open dispatch keeps its worker running. - The restart continuation is a send; Resume all holds each slot until the message is handed over or rejected. - A read error never replaces a loaded transcript, and shows the host's own words. * 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. * fix(native-chat): a request that failed reads as failed A structured chat whose only message the agent's start refused read as a green finish, and a cancelled structured turn did too: the host published a verdict only for turn records, and structured rows carried no `interrupted`. The host projection now reads the session's latest request: its turn's outcome, or `failure` for a send the agent or its start refused. A send that was withdrawn, or left undelivered by a restart or a close, fails nobody and makes nothing listable. The ingest publishes `interrupted` as the hook lanes do, and every reader decodes the verdict through one accessor, so a failure reads Failed on the dot, the rollups, history and `worktree ps`, behaves like a cancellation in every clean-finish policy, and notifies as "failed". * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): a verdict change republishes the mobile status projection * refactor(native-chat): the store's retention trigger keeps its flag compare A verdict change always moves the completion clock the same check already reads, so a second verdict compare there caught nothing new. * test(native-chat): a user message the provider journaled keeps its session listed * 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 * fix(native-chat): a restart offer ends when the chat's agent starts again The offer used to end only when the chat's newest user message changed, because opening a chat started its agent and that start could not be told apart from real activity. Opening a chat starts nothing now, so the host reads the fact it already publishes: a chat's status row goes from not host-owned to host-owned exactly when its agent is started. At that edge the offer and any failure record for the chat are withdrawn, unless the start is a resume action's own (its continuation is the oldest undelivered message). A continuation and a message racing to be first are decided at acceptance: the continuation is refused, quietly and with nothing filed, when any other message was accepted since the restart. A failed continuation start leaves the offer retryable, and each resume action sends its own message id. Deleted: the newest-user-message comparison, its journal reader, the continuation filter, and the failure ledger's own "answered by the chat" check. The marker still carries its message id for one release, so the previous build can read it. * fix(runtime): end a transcript stream when its client unsubscribes Desktop: the IPC subscription controller was dropped as soon as the streaming handler returned, which for most streams is right after it binds. A later runtime:unsubscribe then found nothing to abort, so the host kept the subscriber and derived and sent every publish to a channel no one listened to. The controller now lives until the renderer unsubscribes, resubscribes the same id, or goes away. Mobile: disposing an agentSession.subscribe stream now sends agentSession.unsubscribe with the stream's frame id, so the host ends that subscriber and leaves a sibling stream on the same socket running. The direct path now passes the frame id the relay path already passed. * fix(native-chat): a late provider-session update keeps a failed recovery record failed A provider-session heartbeat that rewrites a completed recovery record kept its interrupted flag but dropped the outcome it was copied with, so a live failed checkpoint read as a clean finish until the next status write. * 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`. * test(native-chat): the terminal-bell check asserts the renamed verdict field The bell notification test still checked for agentInterrupted, which no longer exists, so it could not catch a verdict leaking into a bell dispatch. * fix(native-chat): a failed turn ranks like a completion for attention Attention readers (completion time, Smart Sort, sticky retention, Cmd+J Recent) now demote only a turn the user stopped. A failure is news the user has not seen, so it keeps its completion time, ranks in the Done class, stays retained after its pane goes away, and a retained failure reads failed in the worktree rollup instead of done. Clean-finish policy (hibernation, pane ownership, the value moment) still treats a failure like a stop. The retention trigger compares verdicts again: success -> failure no longer moves the completion clock. * fix(native-chat): one fact ends a restart offer: the chat moved on since the restart The offer is live while no other message has been accepted in the chat since the restart and its agent has not proved a start since. The offer list, the resume's reservation check and the continuation's acceptance check all read that one fact, so a message whose start then failed withdraws the offer too, and a stale click finds nothing to act on. The fact is read off the conversation's open handle, which the restart closed, so it is retired durably whenever it may have changed: a message accepted, a start proven. A close and reopen within the same run therefore cannot bring the offer back. A continuation rejected before it reached the agent does not count, so a retry after a failed start still runs. Deleted: the quit-time gate on withdrawal, which changed nothing because the withdrawal and the quit's own offer write share one queue; the per-action "withdrawn" flag and the separate acceptance check it paired with. * test(native-chat): an older build reads the restart offer this build records The offer lives in a file the previous release reads after a downgrade. Pin that against the pinned release's own capsule, and run the lane when the marker or the capsule changes. * fix(native-chat): read a restart offer against where the journal stood when it was taken "Since the restart" was read off the conversation's open handle, which the idle sweep closes: after a reopen, a message the user had already sent looked older than the handle and the withdrawn offer came back. The offer now records the journal position (epoch and sequence) at the moment it is taken, and a message accepted after that position, or a journal on another epoch, means the chat moved on. That is derived from the journal, so it holds across any number of closes and reopens. An older build's offer has no position; only a start withdraws it. Because the message half is now durable, the offer is no longer rewritten in the recovery file on every accepted message; a proven start still writes it, since only the host that saw the start knows of it. * test(native-chat): wait for the listing's retire write before reading the recovery file * refactor(native-chat): every journal row states which turn it belongs to Rows gain a turn scope stated by the write that creates them: the open root turn, or the conversation. A queued message takes its scope from its handover. Rows stored before scopes existed are placed on replay by the root turn open when they were created, so no persisted state is needed for them. Rewind keeps each retained row's scope and producer, so a subagent's row stays its own. * fix(native-chat): keep the terminal-backed chat's read error over its local echoes Messages winning over a read error is right for the structured chat, whose read retries and whose messages came from the transcript. The terminal-backed view assembles its list from local echoes too (a launch prompt, a pending send), so a failed read there showed only those bubbles and no error. Only the structured pane now keeps messages over an error. * fix(native-chat): a start retries the exit settlement a failed journal write left owed An agent exit whose journal settlement write failed releases the lease latched until a retry lands. Reopening the chat used to be that retry; with reveal now only opening the journal, nothing retried it before the next app launch, and every send was refused. The start the send needs now runs the retry first, where the attach would. * fix(native-chat): a failed main agent reads failed while its subagents still work The verdict is now read from the main agent's own state, not the folded row: a main agent that is done and failed has a verdict even while its subagents keep the row working. Without mainAgent (history, worktree ps, older hosts) the old combined-done rule stands. Display marks the verdict through agentVerdictDisplayMark: a failure outranks every combined state on the agent's dot, label, tab badge, dashboard and activity rows; a stop marks only a done row, so a successful or stopped main agent with live subagents still reads working. Subagent rows keep their own state. The worktree card, terminal tab and Cmd+J rollups share one pane fold and rank a pending question, then failed, then working, monitoring, interrupted and done. worktree ps publishes the main agent's outcome on a working row, and the mobile mirror reads it. The store's change check, the paired-client mirror's equality and its epoch now see a verdict change on a working row, which otherwise moves no state or clock and left the worktree card reading working. Clean-finish policy is unchanged: a working row is never hibernated and has no completion time. * perf(native-chat): answer the owner check without opening the chat Worktree activation calls agentSession.handoffStatus for every chat tab in the worktree, and the answer comes from the session record alone. Reaching it through the accessor opened each resting chat's journal (a full read, the crash-boundary write and a restored status publish), then kept it open for the idle window. It now checks the record and the adapter's support, as before this series, and opens nothing. * fix(native-chat): a read waiting on the session lock opens nothing once quit began The accessor checked for quit before queueing the open, so a read queued behind a session task ran its open after teardown had begun and indexed a journal no teardown step would close. The check now runs at the open itself. * test(native-chat): pin stated turn scopes, the upcast of unscoped rows, and rewind attribution * fix(native-chat): /compact is a message the chat sends, run as a turn of its own The conversation command RPC now accepts /compact into the queue like any send and answers once it is handed over. The delivery loop opens the command's own turn, starts the provider on it, and waits for the provider's end off the session's queue, so messages typed meanwhile are held and delivered after it, even when it fails. It settles by re-reading the journal: a child that died meanwhile already wrote the verdict. Stop ends the command at once. The 180 s completion window, the unconfirmed row and the recovery of an older build's compaction record are gone; that record no longer gates anything. On Codex the provider turn the command opens is claimed into the command's turn. * fix(native-chat): read a failed resume's chat before calling it retryable Whether a failed resume is retryable is the offer's own rule: the chat has not moved on since the restart, read from its journal. The failure list read it only for a chat already open, so once the idle sweep closed a chat the user had moved on in, its failure showed Retry again, and the click did nothing. The list now opens the failed chats first, as the offer list does. * test(native-chat): type the provider event sink the settlement test reaches for * docs(native-chat): the worktree ps outcome comment no longer claims old hosts send it The field is new: an old host sends no outcome at all, so a reader falls back to interrupted. The removed clause said old hosts send it on done rows, which never shipped. * fix(native-chat): say the structured read keeps trying only where it does The structured pane's "Orca keeps trying to load it" line never showed: the view state filled in an untranslated fallback whenever the read error had no text, and the empty state prefers any message. The view state now leaves the message out, so the structured pane shows that line and the terminal-backed pane its own translated one. Mobile's structured lane does not resubscribe after an error frame, so it no longer makes the claim. * fix(native-chat): rows group under the turn their record names, not the one above them Each row's turn is the turn its stated scope names, anchored on the entry that opened it, or on the turn itself when the provider opened it unasked. So /compact groups its own rows and the previous turn is untouched, a message typed into a running turn joins it, and a provider-resumed turn folds under its own Worked-for. A row reporting how a turn ended, an error or the compaction separator, never folds. Desktop and mobile read the same keys; a host that states no scope keeps today's positional grouping. * test(native-chat): await the send's settlement instead of polling for the start The at-rest send tests polled for the provider start with vi.waitFor's one-second default, which a loaded machine outran. They now await the host's own settlement of the message. * docs(native-chat): the status-store listing rule names provider-journaled user messages * fix(native-chat): a restart offer resumes any time after the quit, and knows its own continuations The continuation's message id was dated by the quit, and the ledger refuses a new id dated more than a day back, so Resume or Retry a day after quitting was always refused (on main too). It is now dated by the resume action. Telling a rejected continuation from the user's own message read the operation ledger, whose rows expire after about a day; after that a failed resume stopped being retryable. The offer now records the continuation each action sends on its own capsule entry, bounded to the newest 16, so the ids end with the offer. The ledger read is deleted. * fix(native-chat): a /compact is not a request the sidebar, notifications or restart resume report The sidebar's prompt, preview, verdict and instant, the turn-completion feed, and the restart-resume marker read past a conversation command and its turn to the last real request, so a /compact neither notifies nor re-dates the row, and a command in flight is never offered as work to resume. An older client shown a command's turn in the legacy form names the session's own agent. * fix(orchestration): route no mail to a structured worker its orchestration released A structured worker is routed on ownership, and a resting worker's lease is released, so ownership held while its chat tab stayed listed. A worker the coordinator abandoned and then released, found at rest by the release, therefore still took peer mail and @worktree: broadcasts, and each one restarted its agent. Routing now also reads the orchestration's own resource row: once it is released, direct mail, group addressing and worker-show's addressable answer drop the worker, as they would a terminal worker whose terminal closed. The chat tab stays, and nothing new is stored. * fix(native-chat): a failed retry names the user's prompt, not Orca's continuation A resume's continuation is written to the chat before its start, so after a failed attempt the chat's newest user message is that rejected continuation. A second failure then showed Orca's own restart text as the chat's prompt. A retry now keeps the prompt its first failure named. * test(native-chat): pin what a conversation command's admission refuses at rest and at handover * test(native-chat): tests merged from the base state which turn their rows belong to * fix(native-chat): a refused send notifies failed through the completion feed The host's completion feed followed only the newest turn, so a send the agent or its start refused, which creates no turn, read Failed on its row but sent no notification. The feed now follows the session's latest request, read from the projection the status feed already makes for the commit: a turn keeps its id, a refused send is named by its journal item key. It announces only while the session is idle, as the row reports a verdict, so queued sends refused one commit at a time notify once, and a withdrawn send falls back to a request already announced. * fix(orchestration): read the released row optionally, as the authority does worker-show's observation called the row lookup directly, which a runtime double without it threw on and failed the structured tab-retirement release. * chore(native-chat): one import per module and no unexplained casts in the turn-scope changes * test(claude): pin which turn a Claude row joins, including a subagent's after the turn ends * fix(native-chat): the status bar drops a restart offer the chat moved on from The renderer re-read the host's restart offer only when a failed chat showed activity, so after a message withdrew a pending offer the host answered no chats while the status bar kept counting one, and clicking it opened nothing. The same watch now covers pending offers: a status change in an offered chat asks the host again, once. * fix(native-chat): a refused steer is read from the turn its handover named The latest-request reader decided whether a refused send had joined a running turn by comparing host clocks: its handover time against the previous turn's end. The handover row now states the turn it delivered into, so the reader reads that instead and the clock comparison goes. A journal written before handover rows stated a turn is scoped on replay from the turn open when each row was written, which can differ from the clock reading only when a send and a turn's end share a millisecond. * fix(mobile): the native-chat controller contract carries the turn journal The controller and overlay already pass nativeChatTurnJournal, but the contract type never declared it, so mobile failed to typecheck. * fix(native-chat): the live turn is the running turn, not the newest user row A turn the provider opened on its own (a background wake, a resumed turn) anchors on its own record, but the list still treated the newest user row as the live turn. While such a turn ran, the settled user turn before it lost its duration and the running turn's own rows were drawn as settled, so its tool calls lost their live state. nativeChatTurnMembership now answers both questions from the turn record: each row's turn, and the live turn (the running root turn's anchor, else the newest user row, which is also all an unscoped host has). Desktop and mobile key liveness, the timing clock and the live status's row on it. * test(native-chat): a turn the provider opened keeps its own clock Pins that the local turn clock follows the live turn, so a wake after a settled turn does not restart that turn's clock when no host durations are recorded. * fix(native-chat): a running turn no message opened draws its status on no row Its live status belongs to the transcript-tail indicator alone. Once it settles, its duration draws at its first row as before; a running turn a message opened still draws on that message. * fix(native-chat): every copy of a row carries the main agent's own status History entries, sleep records and `worktree ps` rows carried a flattened top-level `outcome`, copied under different gates and without the main agent's clock. They now carry `mainAgent` (state, outcome, stateStartedAt), the type the live row already persists and sends, and every copy site takes it with `interrupted` through one function, `agentVerdictFields`. - The accessor reads `mainAgent` then the legacy flag; the mobile mirror matches it line for line. - Sleep records admit `mainAgent` with `normalizeMainAgentStatusField`, so a malformed value drops the field, never the record. - Mobile dates a main agent that failed under live subagents by its own clock, as desktop does, and its row equality compares `mainAgent`. - The activity feed reads a history entry's own `mainAgent` instead of rebuilding one; the sync key and history equality compare it. * test(native-chat): pin the worktree ps verdict across host and phone versions Pairs the real v1.4.212 host and phone row reader with this build: an old phone reads a new host's rows by `interrupted`, a new phone reads an old host's rows (no `mainAgent`) the same way, and a new phone reads a failure under live subagents as Failed, dated by `mainAgent.stateStartedAt`. The release checkout now carries the phone's self-contained row reader, and the lane runs when the `worktree ps` row producers change. * test(mobile): name the parity table's row for its role * test(native-chat): a roster of idle or finished children does not keep an agent awake The sweep reads owed background work through the shared child-work liveness that upstream's release clock adopted; a child that went idle or finished is not work the agent still owes. * fix(native-chat): a request that settles while the user is asked something notifies once The completion edge waited for an idle session, and a pending prompt (including a subagent's approval) is not idle. Structured chat has no other attention producer, so a main turn that finished while a subagent waited on the user sent nothing until the prompt was answered. The edge now waits only on owed work (a running turn or an unanswered send), which the projection reports even beneath a pending prompt. A request that settles with a prompt pending announces once; the renderer words it "needs input" from the host status mirror's `attention`, and answering the prompt keeps the same request identity, so it does not announce again. The wire shape is unchanged. * fix(orchestration): a task dispatched into a resting structured worker keeps it running The sweep's open-dispatch check read only the worker-start dispatch that owns the worker's terminal resource, so a task later dispatched to the same worker (orchestration dispatch --to, which writes a dispatch with no worker row) did not count: after thirty quiet minutes the worker was stopped while that task was open, and its coordinator read exited. Any unsettled dispatch addressed to the worker's process incarnation now counts, derived from the existing rows. * fix(native-chat): a command's wait ends when its child does The delivery loop waited for a /compact only on the adapter's compaction tracker, which learns of the child's end only on some exit paths: a Codex exit or close, and a Claude close, never reach it. The wait then never ended, so nothing queued behind the command was delivered again, Stop had no child to answer through, and the tracker's leftover entry refused the next /compact. Every way a child ends passes endProviderChild, so the host now offers a per-child end signal there. The loop races the tracker against it (the dead-generation settlement has already written the command's verdict), and on that end asks every adapter to release the command, so a later command runs and no later provider turn is claimed into the dead one. The adapters' own exit-time releases were unreachable (Codex) or covered one path of several (Claude), and are removed. The Codex RPC test harness moves to its own module so the exit can be driven through the real adapter's connection callback. * fix(native-chat): keep refusing sends during a command on an older host An older host's controller still refuses a send while a conversation command runs, so dropping the client's block turned every message typed during /compact into a 'not sent' row with Retry there. The block stays for hosts that do not run the command as a send-path turn, and goes only for those that do. The signal is one the client already holds: a host that runs /compact on the send path states a turn scope on every journal row it writes, the same fact turn membership uses to tell it from an older host. Both now read it from one predicate. On an empty conversation, or one whose rows all predate the upgrade, the signal is absent until the command's own entry streams in, so that brief window keeps the old local refusal; no capability or wire field is added. * docs(native-chat): comments stop describing the hold this PR removed Eight comments still justified orderings and teardown choices by a viewer or dispatch hold that pinned the provider child. Nothing holds any more; the orderings stand for the binding's redrive subscription and parked mail, and a chat's agent runs from a send until the idle sweep rests it. Comment-only. * fix(native-chat): the completion says when the user is being asked A request that settles while a prompt waits on the user was worded "needs input" from the renderer's status-feed mirror. Remote clients receive the status and completion streams over separate sockets, so they can arrive in either order and the wording could be wrong both ways. The host already knows at emit time, so the completion now carries an optional `awaitingUser: true` in that case and omits it otherwise. The renderer words the notification from that field alone and no longer reads the status mirror. Old clients ignore the field and word by outcome; old hosts never send it. * fix(native-chat): a restart offer keeps the start its own continuation made Whose start ended an offer was decided at read time, from whether the offer's continuation was still the queued message. Once the provider refused that continuation, the child it had started read as someone else's start, so the offer ended and its failure showed no Retry. The delivery loop now records which queued message a start is for on the in-memory child, and the child's end carries it; the offer counts a start as its own when that message is one of its continuations. * fix(native-chat): a rewound turn still names the message that opened it A Codex rewind rebuilds the epoch without submissions, so each sent message survives only under its provider key. The kept turn records still named the submission key, so each turn anchored on itself and its rows grouped apart from the message that opened it. The rewind now renames the turn's opener along with the message. * fix(native-chat): Stop ends only the command it names Stop on a command turn abandoned whatever compaction the session had pending, so a late Stop for an earlier /compact cancelled the one running now. The tracker now ends a command only when the Stop names its turn, and the cancel reply reports whether it did. * fix(native-chat): an agent gets a full idle window after its owed work ends The sweep measured quiet only from the last journal row, so once a subagent, command, monitor or dispatch that had outlived the window ended, the agent was stopped at the next tick. A child can read done before the lead's wake-up turn writes anything, and stopping in that gap loses the wake-up. The sweep now counts owed work it observes as activity, which gives the agent the full window afterwards, as the release clock it replaced did. * test(claude): the options-read fixture runs a live child The fixture marked its conversation running with a hasProviderChild field the session type does not have, so the read took the at-rest path and refused a session with no record. It now carries a child, which is what the read checks. * test(native-chat): host tests reach its collaborators through a typed seam The rest-test rig and three test files read the host's private members with Reflect.get and cast the result. The host now exposes one test-only accessor, collaboratorsForTests(), and the subscribers class a subscriberCountForTests() beside its existing retainedActivityCountForTests(), so the tests are checked against the real types and the casts are gone. * fix(worktree-status): a departed agent's failure yields to live work on the worktree card A retained failed agent has no expiry, so ranking it with a live failure pinned the card to Failed over other panes' live work. It now ranks below working, monitoring and permission, and above every finished outcome. * refactor(orchestration): one owner answers a structured worker's custody Routing, group addressing, worker-show and the idle sweep each composed their own reading of whether orchestration still holds a structured worker, so each new obligation or retirement state had to be added to every reader. structured-worker-custody now derives both answers from the worker-terminal list state coordinators see in worker-list: addressable is owned and not released, and owed work is an active custody or an unsettled task dispatched to the same incarnation. The owner's state is read through the remote dispatch attachment too, as the terminal transfer lookup already does. Behaviour is unchanged; a settled worker awaiting its coordinator still rests. * refactor(orchestration): owed work is an open dispatch on the worker's incarnation A supervised worker's own dispatch context stays open exactly while the worker is active, so the separate active-custody branch only repeated it. Owed work is now one fact, which also states the policy that a worker awaiting its coordinator's decision may rest, and both custody decisions are written once at the top of the module. * docs(agent-status): a departed agent's failure ranks below live work on the worktree card * fix(native-chat): a restart offer knows its continuations by a tag in their id The offer recorded each continuation id in a list on its capsule entry, capped at 16, and a running action's id in memory. Both could disagree with the journal: past the cap an old rejected continuation read as the chat moving on, and a crash during a retry restored the failure's older entry, which lacked the retry's id. Each continuation id now carries a tag derived from the offer (its teardown and chat), then the action's own part, so any continuation of this offer, queued or rejected, is recognised from the journal row and the marker alone. The persisted list, its cap and the in-memory action map are deleted; the agent-start withdrawal keeps an offer whose own continuation the start was for, read against the stored marker. * test(runtime): the legacy-worker reveal test judges its stale snapshot inside the wait The tui-idle probe reads through readTerminal, which now awaits the structured worker check before the PTY read, so the probe's snapshot request starts a microtask later. vi.waitFor missed it on its first check and polled again at 50 ms, the same moment the wait's own 50 ms timeout fired. The stale snapshot then resolved after the wait had already timed out, so the test passed without judging it, and the rejection landed before any handler was attached. Vitest reported that as an unhandled error and failed the shard. Polling every 1 ms sees the request within a few ms, so the snapshot is judged while the wait is still pending. * fix(native-chat): a message held behind /compact is drawn where it was handed over A message typed while /compact runs was drawn above the compaction's result, between itself and its own answer. The reducer kept every item at the sequence and timestamp of the row that created it, and a queued message is created at acceptance, long before the command it waits behind writes its result. The phone orders by that sequence and the desktop by that timestamp, so both put the message first. A queued message now takes its position from its handover row, the same row that already states its turn scope. Everything the agent did before the handover, a command it waited behind included, draws above it. This holds for every held message, not only /compact's, and needs no client change: every client, older builds included, reads the position the host publishes. A live batch already carries the item when its dispatch row lands, and history pages cut the reduced timeline by sequence, so paging stays contiguous. * fix(native-chat): a phone's send during /compact answers without waiting out the compaction A client that predates accepted-send replies, which is every phone build, has its send reply held until the host hands the message over. A message sent during /compact is not handed over until the compaction ends, so the phone's 15 s request timeout fired first and showed the message as unconfirmed. That wait now also ends once the message is queued behind a running command. This is read from the journal's running turn and needs no new state. Every other wait still ends at the handover: behind a starting child or an ordinary turn, and for restart resume, the command front door and orchestration, which keep the plain handover point. * perf(native-chat): a rewind places provider items with one pass over the merged rows A Codex rewind gives each provider item the old epoch never held the turn record for its provider turn. It found that record by scanning every merged row, restoring each row's body, once per provider item. That is quadratic, and it runs on the host's main thread up to the journal's 10,000-row cap, twice per rewind. A rewind record written before rows carried their scope holds no scope for any provider item, so it paid the full cost. The merge now indexes turn records by provider turn id once, keeping the first match as the scan did, and each provider item looks its record up. * 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. * fix(native-chat): a message waiting behind /compact is drawn after it until it is sent A message sent while /compact runs is placed where it was handed over. It was still drawn where it was accepted until then. /compact writes its result one step before the handover, so for that step the waiting message sat above the compaction's separator. A message the host accepted but has not handed over is not part of the conversation yet, so both clients now draw it after everything the agent has done. The shared projection moves it to the end, which is the order the phone draws. The desktop ranks it with the other not-yet-sent rows, after the streaming preview. At handover it takes its place from its handover row, which is also after the separator, so it never appears above the compaction it waited for. * fix(native-chat): the idle sweep reads owed work every tick Owed work counted as activity, but the sweep read it only once the idle window had elapsed, so it refreshed the clock at most once a window. Work that ended just before the next read left the agent to be stopped at that read, moments after the work ended, which is the gap the refresh was meant to cover. The sweep now reads owed work on every tick for a started agent, so the window always runs from the last tick that saw work owed. * fix(native-chat): a continuation handed to the agent stays sent The offer read its own continuation as not reaching the agent while its dispatch was pending, which also covered one already handed over and still unanswered. When the wait for that answer ended first, the failure it filed read as retryable, and a retry sent a second continuation to an agent that may have acted on the first. Only a continuation still queued, or rejected, is now read as unsent. * 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(cross-version): load the phone row readers without mobile's toolchain Vite transforms a file against its nearest tsconfig, and mobile/tsconfig.json extends expo/tsconfig.base.json, which the root-only cross-version lane never installs. The worktree ps verdict suite imported the current phone row reader from mobile/ directly, so CI failed with TSConfckParseError before any test ran. The harness now imports a copy of the working-tree reader placed under the checkout cache, where the root tsconfig applies, as it already does for the release checkout's copy. Both readers are still the real files. * test(cross-version): keep the checkout path-guard message and justify the copy import's cast * fix(native-chat): a command ends only by its own provider answer or its child's end Stop no longer settles a conversation command. It interrupts it like any turn, and when the provider cannot take that (Codex has not opened the command's turn yet, or Claude refuses the interrupt) it stops the child, whose dead-generation settlement writes the verdict. The pending command now lives on the provider child's own session instead of an adapter-wide map keyed by session, so it dies with the child and nothing has to release it. Claude's /compact is sent under a uuid the slot records, and only a root result naming that input (or naming none) ends it; its outcome is read with the ordinary result reading, so a stopped /compact is a cancellation. * fix(native-chat): a command's settle answers its message before ending its turn The two writes are not one batch. Writing the message's answer first means a crash between them leaves a running command turn, which the stale-turn sweep already settles, instead of an ended turn whose message reads as in flight forever. The settle now writes only while the command turn is still running. * fix(native-chat): "Worked for" counts from the handover, not the send A message held behind /compact, or behind a cold start, used to count the wait as the agent's work, although its row is drawn at the handover. Every handed-over submission's turn, the command's own included, now starts at the handover row's instant, falling back to the send time for a host that recorded none. * test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * test(native-chat): the interrupted create's own retry continues again The merge of main's lease-latch fix replaced that test's retry of the interrupted create, under its own operation id, with a fresh start whose result nothing read. That fresh start passes with the released-reservation continuation deleted, so the case the fix exists for went untested. The retry and its assertion are main's again. * docs(native-chat): three comments that still had views starting agents A start with nothing queued now comes from a command, goal change or rewind; an interrupted compaction left alone would refuse every send, so no agent would ever start to finish it; and a current host raises the unattached read refusal only once quit began, with the attach window belonging to an older host. * 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): a second Stop on a command e…
…tablyai#23923) * refactor(shared): move onboarding, notification and terminal platform defaults out of constants.ts * chore(lint): keep the shapedSidebar naming exemption on the file that now holds it * chore(i18n): regenerate the runtime catalog so it covers main's shipped keys
…plied (stablyai#23812) * fix(browser): derive a tab's phone identity from the viewport actually applied * fix(browser): record the applied viewport before the touch step so a detach cannot be overwritten * chore(browser): scope the applied-viewport comment to preset writes
… is resized (stablyai#23852) * fix(browser): never resize a browser tab whose page crashed, which segfaulted Orca Chromium's Emulation.setDeviceMetricsOverride and Emulation.setVisibleSize resize the page's view without checking it still exists (WebContentsImpl::SetDeviceEmulationSize). A crashed page renderer takes its view with it until a reload builds a new one, so either command sent in that gap killed Orca's whole main process with SIGSEGV. One gate, sendGuestCdpCommand, now refuses those two commands while the tab's renderer is gone. The viewport preset writer, the agent viewport command, the agent bridge's command sender, the CDP proxy and the screencast device metrics all send through it. The check runs in the same task as the send, so the renderer cannot die in between. The page's own reload reapplies the chosen preset on dom-ready, as it already did. * test(browser): type the CDP gate's target so its test double needs no cast CI's changed-lines gate rejects new type assertions. The gate only uses isDestroyed, isCrashed and debugger.sendCommand, so it now takes exactly that shape; the viewport test keeps the one fixture cast its siblings use, with a line-specific SAFETY note.
…ger locks the chat (stablyai#23524) * 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. * fix(native-chat): the conversation outlives its agent Opening a chat no longer starts its agent. A conversation is reached through one host accessor that opens its journal at rest, and a send is what starts the agent, through the delivery loop. One idle sweep, every five minutes, stops an agent that has been quiet for thirty minutes and owes no work, then drops an open journal handle that is only a cache. Its record, tab, status row and readers stay. - hold and release are no-ops; hold still builds the host for shipped mobile builds. - The holders, the holds, the release clock and the exit respawn are deleted. - Options, the model list, the goal and the context meter answer at rest; a model pick at rest is recorded as intent for the next start. - Compact, rewind, clear and goal changes start the agent first. A send does too when a rewind is still in doubt after the conversation opens. - Orchestration routes mail and group addresses on ownership (the record plus the chat tab), not on whether the process runs. An open dispatch keeps its worker running. - The restart continuation is a send; Resume all holds each slot until the message is handed over or rejected. - A read error never replaces a loaded transcript, and shows the host's own words. * 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. * fix(native-chat): a request that failed reads as failed A structured chat whose only message the agent's start refused read as a green finish, and a cancelled structured turn did too: the host published a verdict only for turn records, and structured rows carried no `interrupted`. The host projection now reads the session's latest request: its turn's outcome, or `failure` for a send the agent or its start refused. A send that was withdrawn, or left undelivered by a restart or a close, fails nobody and makes nothing listable. The ingest publishes `interrupted` as the hook lanes do, and every reader decodes the verdict through one accessor, so a failure reads Failed on the dot, the rollups, history and `worktree ps`, behaves like a cancellation in every clean-finish policy, and notifies as "failed". * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): a verdict change republishes the mobile status projection * refactor(native-chat): the store's retention trigger keeps its flag compare A verdict change always moves the completion clock the same check already reads, so a second verdict compare there caught nothing new. * test(native-chat): a user message the provider journaled keeps its session listed * 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 * fix(native-chat): a restart offer ends when the chat's agent starts again The offer used to end only when the chat's newest user message changed, because opening a chat started its agent and that start could not be told apart from real activity. Opening a chat starts nothing now, so the host reads the fact it already publishes: a chat's status row goes from not host-owned to host-owned exactly when its agent is started. At that edge the offer and any failure record for the chat are withdrawn, unless the start is a resume action's own (its continuation is the oldest undelivered message). A continuation and a message racing to be first are decided at acceptance: the continuation is refused, quietly and with nothing filed, when any other message was accepted since the restart. A failed continuation start leaves the offer retryable, and each resume action sends its own message id. Deleted: the newest-user-message comparison, its journal reader, the continuation filter, and the failure ledger's own "answered by the chat" check. The marker still carries its message id for one release, so the previous build can read it. * fix(runtime): end a transcript stream when its client unsubscribes Desktop: the IPC subscription controller was dropped as soon as the streaming handler returned, which for most streams is right after it binds. A later runtime:unsubscribe then found nothing to abort, so the host kept the subscriber and derived and sent every publish to a channel no one listened to. The controller now lives until the renderer unsubscribes, resubscribes the same id, or goes away. Mobile: disposing an agentSession.subscribe stream now sends agentSession.unsubscribe with the stream's frame id, so the host ends that subscriber and leaves a sibling stream on the same socket running. The direct path now passes the frame id the relay path already passed. * fix(native-chat): a late provider-session update keeps a failed recovery record failed A provider-session heartbeat that rewrites a completed recovery record kept its interrupted flag but dropped the outcome it was copied with, so a live failed checkpoint read as a clean finish until the next status write. * 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`. * test(native-chat): the terminal-bell check asserts the renamed verdict field The bell notification test still checked for agentInterrupted, which no longer exists, so it could not catch a verdict leaking into a bell dispatch. * fix(native-chat): a failed turn ranks like a completion for attention Attention readers (completion time, Smart Sort, sticky retention, Cmd+J Recent) now demote only a turn the user stopped. A failure is news the user has not seen, so it keeps its completion time, ranks in the Done class, stays retained after its pane goes away, and a retained failure reads failed in the worktree rollup instead of done. Clean-finish policy (hibernation, pane ownership, the value moment) still treats a failure like a stop. The retention trigger compares verdicts again: success -> failure no longer moves the completion clock. * fix(native-chat): one fact ends a restart offer: the chat moved on since the restart The offer is live while no other message has been accepted in the chat since the restart and its agent has not proved a start since. The offer list, the resume's reservation check and the continuation's acceptance check all read that one fact, so a message whose start then failed withdraws the offer too, and a stale click finds nothing to act on. The fact is read off the conversation's open handle, which the restart closed, so it is retired durably whenever it may have changed: a message accepted, a start proven. A close and reopen within the same run therefore cannot bring the offer back. A continuation rejected before it reached the agent does not count, so a retry after a failed start still runs. Deleted: the quit-time gate on withdrawal, which changed nothing because the withdrawal and the quit's own offer write share one queue; the per-action "withdrawn" flag and the separate acceptance check it paired with. * test(native-chat): an older build reads the restart offer this build records The offer lives in a file the previous release reads after a downgrade. Pin that against the pinned release's own capsule, and run the lane when the marker or the capsule changes. * fix(native-chat): read a restart offer against where the journal stood when it was taken "Since the restart" was read off the conversation's open handle, which the idle sweep closes: after a reopen, a message the user had already sent looked older than the handle and the withdrawn offer came back. The offer now records the journal position (epoch and sequence) at the moment it is taken, and a message accepted after that position, or a journal on another epoch, means the chat moved on. That is derived from the journal, so it holds across any number of closes and reopens. An older build's offer has no position; only a start withdraws it. Because the message half is now durable, the offer is no longer rewritten in the recovery file on every accepted message; a proven start still writes it, since only the host that saw the start knows of it. * test(native-chat): wait for the listing's retire write before reading the recovery file * refactor(native-chat): every journal row states which turn it belongs to Rows gain a turn scope stated by the write that creates them: the open root turn, or the conversation. A queued message takes its scope from its handover. Rows stored before scopes existed are placed on replay by the root turn open when they were created, so no persisted state is needed for them. Rewind keeps each retained row's scope and producer, so a subagent's row stays its own. * fix(native-chat): keep the terminal-backed chat's read error over its local echoes Messages winning over a read error is right for the structured chat, whose read retries and whose messages came from the transcript. The terminal-backed view assembles its list from local echoes too (a launch prompt, a pending send), so a failed read there showed only those bubbles and no error. Only the structured pane now keeps messages over an error. * fix(native-chat): a start retries the exit settlement a failed journal write left owed An agent exit whose journal settlement write failed releases the lease latched until a retry lands. Reopening the chat used to be that retry; with reveal now only opening the journal, nothing retried it before the next app launch, and every send was refused. The start the send needs now runs the retry first, where the attach would. * fix(native-chat): a failed main agent reads failed while its subagents still work The verdict is now read from the main agent's own state, not the folded row: a main agent that is done and failed has a verdict even while its subagents keep the row working. Without mainAgent (history, worktree ps, older hosts) the old combined-done rule stands. Display marks the verdict through agentVerdictDisplayMark: a failure outranks every combined state on the agent's dot, label, tab badge, dashboard and activity rows; a stop marks only a done row, so a successful or stopped main agent with live subagents still reads working. Subagent rows keep their own state. The worktree card, terminal tab and Cmd+J rollups share one pane fold and rank a pending question, then failed, then working, monitoring, interrupted and done. worktree ps publishes the main agent's outcome on a working row, and the mobile mirror reads it. The store's change check, the paired-client mirror's equality and its epoch now see a verdict change on a working row, which otherwise moves no state or clock and left the worktree card reading working. Clean-finish policy is unchanged: a working row is never hibernated and has no completion time. * perf(native-chat): answer the owner check without opening the chat Worktree activation calls agentSession.handoffStatus for every chat tab in the worktree, and the answer comes from the session record alone. Reaching it through the accessor opened each resting chat's journal (a full read, the crash-boundary write and a restored status publish), then kept it open for the idle window. It now checks the record and the adapter's support, as before this series, and opens nothing. * fix(native-chat): a read waiting on the session lock opens nothing once quit began The accessor checked for quit before queueing the open, so a read queued behind a session task ran its open after teardown had begun and indexed a journal no teardown step would close. The check now runs at the open itself. * test(native-chat): pin stated turn scopes, the upcast of unscoped rows, and rewind attribution * fix(native-chat): /compact is a message the chat sends, run as a turn of its own The conversation command RPC now accepts /compact into the queue like any send and answers once it is handed over. The delivery loop opens the command's own turn, starts the provider on it, and waits for the provider's end off the session's queue, so messages typed meanwhile are held and delivered after it, even when it fails. It settles by re-reading the journal: a child that died meanwhile already wrote the verdict. Stop ends the command at once. The 180 s completion window, the unconfirmed row and the recovery of an older build's compaction record are gone; that record no longer gates anything. On Codex the provider turn the command opens is claimed into the command's turn. * fix(native-chat): read a failed resume's chat before calling it retryable Whether a failed resume is retryable is the offer's own rule: the chat has not moved on since the restart, read from its journal. The failure list read it only for a chat already open, so once the idle sweep closed a chat the user had moved on in, its failure showed Retry again, and the click did nothing. The list now opens the failed chats first, as the offer list does. * test(native-chat): type the provider event sink the settlement test reaches for * docs(native-chat): the worktree ps outcome comment no longer claims old hosts send it The field is new: an old host sends no outcome at all, so a reader falls back to interrupted. The removed clause said old hosts send it on done rows, which never shipped. * fix(native-chat): say the structured read keeps trying only where it does The structured pane's "Orca keeps trying to load it" line never showed: the view state filled in an untranslated fallback whenever the read error had no text, and the empty state prefers any message. The view state now leaves the message out, so the structured pane shows that line and the terminal-backed pane its own translated one. Mobile's structured lane does not resubscribe after an error frame, so it no longer makes the claim. * fix(native-chat): rows group under the turn their record names, not the one above them Each row's turn is the turn its stated scope names, anchored on the entry that opened it, or on the turn itself when the provider opened it unasked. So /compact groups its own rows and the previous turn is untouched, a message typed into a running turn joins it, and a provider-resumed turn folds under its own Worked-for. A row reporting how a turn ended, an error or the compaction separator, never folds. Desktop and mobile read the same keys; a host that states no scope keeps today's positional grouping. * test(native-chat): await the send's settlement instead of polling for the start The at-rest send tests polled for the provider start with vi.waitFor's one-second default, which a loaded machine outran. They now await the host's own settlement of the message. * docs(native-chat): the status-store listing rule names provider-journaled user messages * fix(native-chat): a restart offer resumes any time after the quit, and knows its own continuations The continuation's message id was dated by the quit, and the ledger refuses a new id dated more than a day back, so Resume or Retry a day after quitting was always refused (on main too). It is now dated by the resume action. Telling a rejected continuation from the user's own message read the operation ledger, whose rows expire after about a day; after that a failed resume stopped being retryable. The offer now records the continuation each action sends on its own capsule entry, bounded to the newest 16, so the ids end with the offer. The ledger read is deleted. * fix(native-chat): a /compact is not a request the sidebar, notifications or restart resume report The sidebar's prompt, preview, verdict and instant, the turn-completion feed, and the restart-resume marker read past a conversation command and its turn to the last real request, so a /compact neither notifies nor re-dates the row, and a command in flight is never offered as work to resume. An older client shown a command's turn in the legacy form names the session's own agent. * fix(orchestration): route no mail to a structured worker its orchestration released A structured worker is routed on ownership, and a resting worker's lease is released, so ownership held while its chat tab stayed listed. A worker the coordinator abandoned and then released, found at rest by the release, therefore still took peer mail and @worktree: broadcasts, and each one restarted its agent. Routing now also reads the orchestration's own resource row: once it is released, direct mail, group addressing and worker-show's addressable answer drop the worker, as they would a terminal worker whose terminal closed. The chat tab stays, and nothing new is stored. * fix(native-chat): a failed retry names the user's prompt, not Orca's continuation A resume's continuation is written to the chat before its start, so after a failed attempt the chat's newest user message is that rejected continuation. A second failure then showed Orca's own restart text as the chat's prompt. A retry now keeps the prompt its first failure named. * test(native-chat): pin what a conversation command's admission refuses at rest and at handover * test(native-chat): tests merged from the base state which turn their rows belong to * fix(native-chat): a refused send notifies failed through the completion feed The host's completion feed followed only the newest turn, so a send the agent or its start refused, which creates no turn, read Failed on its row but sent no notification. The feed now follows the session's latest request, read from the projection the status feed already makes for the commit: a turn keeps its id, a refused send is named by its journal item key. It announces only while the session is idle, as the row reports a verdict, so queued sends refused one commit at a time notify once, and a withdrawn send falls back to a request already announced. * fix(orchestration): read the released row optionally, as the authority does worker-show's observation called the row lookup directly, which a runtime double without it threw on and failed the structured tab-retirement release. * chore(native-chat): one import per module and no unexplained casts in the turn-scope changes * test(claude): pin which turn a Claude row joins, including a subagent's after the turn ends * fix(native-chat): the status bar drops a restart offer the chat moved on from The renderer re-read the host's restart offer only when a failed chat showed activity, so after a message withdrew a pending offer the host answered no chats while the status bar kept counting one, and clicking it opened nothing. The same watch now covers pending offers: a status change in an offered chat asks the host again, once. * fix(native-chat): a refused steer is read from the turn its handover named The latest-request reader decided whether a refused send had joined a running turn by comparing host clocks: its handover time against the previous turn's end. The handover row now states the turn it delivered into, so the reader reads that instead and the clock comparison goes. A journal written before handover rows stated a turn is scoped on replay from the turn open when each row was written, which can differ from the clock reading only when a send and a turn's end share a millisecond. * fix(mobile): the native-chat controller contract carries the turn journal The controller and overlay already pass nativeChatTurnJournal, but the contract type never declared it, so mobile failed to typecheck. * fix(native-chat): the live turn is the running turn, not the newest user row A turn the provider opened on its own (a background wake, a resumed turn) anchors on its own record, but the list still treated the newest user row as the live turn. While such a turn ran, the settled user turn before it lost its duration and the running turn's own rows were drawn as settled, so its tool calls lost their live state. nativeChatTurnMembership now answers both questions from the turn record: each row's turn, and the live turn (the running root turn's anchor, else the newest user row, which is also all an unscoped host has). Desktop and mobile key liveness, the timing clock and the live status's row on it. * test(native-chat): a turn the provider opened keeps its own clock Pins that the local turn clock follows the live turn, so a wake after a settled turn does not restart that turn's clock when no host durations are recorded. * fix(native-chat): a running turn no message opened draws its status on no row Its live status belongs to the transcript-tail indicator alone. Once it settles, its duration draws at its first row as before; a running turn a message opened still draws on that message. * fix(native-chat): every copy of a row carries the main agent's own status History entries, sleep records and `worktree ps` rows carried a flattened top-level `outcome`, copied under different gates and without the main agent's clock. They now carry `mainAgent` (state, outcome, stateStartedAt), the type the live row already persists and sends, and every copy site takes it with `interrupted` through one function, `agentVerdictFields`. - The accessor reads `mainAgent` then the legacy flag; the mobile mirror matches it line for line. - Sleep records admit `mainAgent` with `normalizeMainAgentStatusField`, so a malformed value drops the field, never the record. - Mobile dates a main agent that failed under live subagents by its own clock, as desktop does, and its row equality compares `mainAgent`. - The activity feed reads a history entry's own `mainAgent` instead of rebuilding one; the sync key and history equality compare it. * test(native-chat): pin the worktree ps verdict across host and phone versions Pairs the real v1.4.212 host and phone row reader with this build: an old phone reads a new host's rows by `interrupted`, a new phone reads an old host's rows (no `mainAgent`) the same way, and a new phone reads a failure under live subagents as Failed, dated by `mainAgent.stateStartedAt`. The release checkout now carries the phone's self-contained row reader, and the lane runs when the `worktree ps` row producers change. * test(mobile): name the parity table's row for its role * test(native-chat): a roster of idle or finished children does not keep an agent awake The sweep reads owed background work through the shared child-work liveness that upstream's release clock adopted; a child that went idle or finished is not work the agent still owes. * fix(native-chat): a request that settles while the user is asked something notifies once The completion edge waited for an idle session, and a pending prompt (including a subagent's approval) is not idle. Structured chat has no other attention producer, so a main turn that finished while a subagent waited on the user sent nothing until the prompt was answered. The edge now waits only on owed work (a running turn or an unanswered send), which the projection reports even beneath a pending prompt. A request that settles with a prompt pending announces once; the renderer words it "needs input" from the host status mirror's `attention`, and answering the prompt keeps the same request identity, so it does not announce again. The wire shape is unchanged. * fix(orchestration): a task dispatched into a resting structured worker keeps it running The sweep's open-dispatch check read only the worker-start dispatch that owns the worker's terminal resource, so a task later dispatched to the same worker (orchestration dispatch --to, which writes a dispatch with no worker row) did not count: after thirty quiet minutes the worker was stopped while that task was open, and its coordinator read exited. Any unsettled dispatch addressed to the worker's process incarnation now counts, derived from the existing rows. * fix(native-chat): a command's wait ends when its child does The delivery loop waited for a /compact only on the adapter's compaction tracker, which learns of the child's end only on some exit paths: a Codex exit or close, and a Claude close, never reach it. The wait then never ended, so nothing queued behind the command was delivered again, Stop had no child to answer through, and the tracker's leftover entry refused the next /compact. Every way a child ends passes endProviderChild, so the host now offers a per-child end signal there. The loop races the tracker against it (the dead-generation settlement has already written the command's verdict), and on that end asks every adapter to release the command, so a later command runs and no later provider turn is claimed into the dead one. The adapters' own exit-time releases were unreachable (Codex) or covered one path of several (Claude), and are removed. The Codex RPC test harness moves to its own module so the exit can be driven through the real adapter's connection callback. * fix(native-chat): keep refusing sends during a command on an older host An older host's controller still refuses a send while a conversation command runs, so dropping the client's block turned every message typed during /compact into a 'not sent' row with Retry there. The block stays for hosts that do not run the command as a send-path turn, and goes only for those that do. The signal is one the client already holds: a host that runs /compact on the send path states a turn scope on every journal row it writes, the same fact turn membership uses to tell it from an older host. Both now read it from one predicate. On an empty conversation, or one whose rows all predate the upgrade, the signal is absent until the command's own entry streams in, so that brief window keeps the old local refusal; no capability or wire field is added. * docs(native-chat): comments stop describing the hold this PR removed Eight comments still justified orderings and teardown choices by a viewer or dispatch hold that pinned the provider child. Nothing holds any more; the orderings stand for the binding's redrive subscription and parked mail, and a chat's agent runs from a send until the idle sweep rests it. Comment-only. * fix(native-chat): the completion says when the user is being asked A request that settles while a prompt waits on the user was worded "needs input" from the renderer's status-feed mirror. Remote clients receive the status and completion streams over separate sockets, so they can arrive in either order and the wording could be wrong both ways. The host already knows at emit time, so the completion now carries an optional `awaitingUser: true` in that case and omits it otherwise. The renderer words the notification from that field alone and no longer reads the status mirror. Old clients ignore the field and word by outcome; old hosts never send it. * fix(native-chat): a restart offer keeps the start its own continuation made Whose start ended an offer was decided at read time, from whether the offer's continuation was still the queued message. Once the provider refused that continuation, the child it had started read as someone else's start, so the offer ended and its failure showed no Retry. The delivery loop now records which queued message a start is for on the in-memory child, and the child's end carries it; the offer counts a start as its own when that message is one of its continuations. * fix(native-chat): a rewound turn still names the message that opened it A Codex rewind rebuilds the epoch without submissions, so each sent message survives only under its provider key. The kept turn records still named the submission key, so each turn anchored on itself and its rows grouped apart from the message that opened it. The rewind now renames the turn's opener along with the message. * fix(native-chat): Stop ends only the command it names Stop on a command turn abandoned whatever compaction the session had pending, so a late Stop for an earlier /compact cancelled the one running now. The tracker now ends a command only when the Stop names its turn, and the cancel reply reports whether it did. * fix(native-chat): an agent gets a full idle window after its owed work ends The sweep measured quiet only from the last journal row, so once a subagent, command, monitor or dispatch that had outlived the window ended, the agent was stopped at the next tick. A child can read done before the lead's wake-up turn writes anything, and stopping in that gap loses the wake-up. The sweep now counts owed work it observes as activity, which gives the agent the full window afterwards, as the release clock it replaced did. * test(claude): the options-read fixture runs a live child The fixture marked its conversation running with a hasProviderChild field the session type does not have, so the read took the at-rest path and refused a session with no record. It now carries a child, which is what the read checks. * test(native-chat): host tests reach its collaborators through a typed seam The rest-test rig and three test files read the host's private members with Reflect.get and cast the result. The host now exposes one test-only accessor, collaboratorsForTests(), and the subscribers class a subscriberCountForTests() beside its existing retainedActivityCountForTests(), so the tests are checked against the real types and the casts are gone. * fix(worktree-status): a departed agent's failure yields to live work on the worktree card A retained failed agent has no expiry, so ranking it with a live failure pinned the card to Failed over other panes' live work. It now ranks below working, monitoring and permission, and above every finished outcome. * refactor(orchestration): one owner answers a structured worker's custody Routing, group addressing, worker-show and the idle sweep each composed their own reading of whether orchestration still holds a structured worker, so each new obligation or retirement state had to be added to every reader. structured-worker-custody now derives both answers from the worker-terminal list state coordinators see in worker-list: addressable is owned and not released, and owed work is an active custody or an unsettled task dispatched to the same incarnation. The owner's state is read through the remote dispatch attachment too, as the terminal transfer lookup already does. Behaviour is unchanged; a settled worker awaiting its coordinator still rests. * refactor(orchestration): owed work is an open dispatch on the worker's incarnation A supervised worker's own dispatch context stays open exactly while the worker is active, so the separate active-custody branch only repeated it. Owed work is now one fact, which also states the policy that a worker awaiting its coordinator's decision may rest, and both custody decisions are written once at the top of the module. * docs(agent-status): a departed agent's failure ranks below live work on the worktree card * fix(native-chat): a restart offer knows its continuations by a tag in their id The offer recorded each continuation id in a list on its capsule entry, capped at 16, and a running action's id in memory. Both could disagree with the journal: past the cap an old rejected continuation read as the chat moving on, and a crash during a retry restored the failure's older entry, which lacked the retry's id. Each continuation id now carries a tag derived from the offer (its teardown and chat), then the action's own part, so any continuation of this offer, queued or rejected, is recognised from the journal row and the marker alone. The persisted list, its cap and the in-memory action map are deleted; the agent-start withdrawal keeps an offer whose own continuation the start was for, read against the stored marker. * test(runtime): the legacy-worker reveal test judges its stale snapshot inside the wait The tui-idle probe reads through readTerminal, which now awaits the structured worker check before the PTY read, so the probe's snapshot request starts a microtask later. vi.waitFor missed it on its first check and polled again at 50 ms, the same moment the wait's own 50 ms timeout fired. The stale snapshot then resolved after the wait had already timed out, so the test passed without judging it, and the rejection landed before any handler was attached. Vitest reported that as an unhandled error and failed the shard. Polling every 1 ms sees the request within a few ms, so the snapshot is judged while the wait is still pending. * fix(native-chat): a message held behind /compact is drawn where it was handed over A message typed while /compact runs was drawn above the compaction's result, between itself and its own answer. The reducer kept every item at the sequence and timestamp of the row that created it, and a queued message is created at acceptance, long before the command it waits behind writes its result. The phone orders by that sequence and the desktop by that timestamp, so both put the message first. A queued message now takes its position from its handover row, the same row that already states its turn scope. Everything the agent did before the handover, a command it waited behind included, draws above it. This holds for every held message, not only /compact's, and needs no client change: every client, older builds included, reads the position the host publishes. A live batch already carries the item when its dispatch row lands, and history pages cut the reduced timeline by sequence, so paging stays contiguous. * fix(native-chat): a phone's send during /compact answers without waiting out the compaction A client that predates accepted-send replies, which is every phone build, has its send reply held until the host hands the message over. A message sent during /compact is not handed over until the compaction ends, so the phone's 15 s request timeout fired first and showed the message as unconfirmed. That wait now also ends once the message is queued behind a running command. This is read from the journal's running turn and needs no new state. Every other wait still ends at the handover: behind a starting child or an ordinary turn, and for restart resume, the command front door and orchestration, which keep the plain handover point. * perf(native-chat): a rewind places provider items with one pass over the merged rows A Codex rewind gives each provider item the old epoch never held the turn record for its provider turn. It found that record by scanning every merged row, restoring each row's body, once per provider item. That is quadratic, and it runs on the host's main thread up to the journal's 10,000-row cap, twice per rewind. A rewind record written before rows carried their scope holds no scope for any provider item, so it paid the full cost. The merge now indexes turn records by provider turn id once, keeping the first match as the scan did, and each provider item looks its record up. * 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. * fix(native-chat): a message waiting behind /compact is drawn after it until it is sent A message sent while /compact runs is placed where it was handed over. It was still drawn where it was accepted until then. /compact writes its result one step before the handover, so for that step the waiting message sat above the compaction's separator. A message the host accepted but has not handed over is not part of the conversation yet, so both clients now draw it after everything the agent has done. The shared projection moves it to the end, which is the order the phone draws. The desktop ranks it with the other not-yet-sent rows, after the streaming preview. At handover it takes its place from its handover row, which is also after the separator, so it never appears above the compaction it waited for. * fix(native-chat): the idle sweep reads owed work every tick Owed work counted as activity, but the sweep read it only once the idle window had elapsed, so it refreshed the clock at most once a window. Work that ended just before the next read left the agent to be stopped at that read, moments after the work ended, which is the gap the refresh was meant to cover. The sweep now reads owed work on every tick for a started agent, so the window always runs from the last tick that saw work owed. * fix(native-chat): a continuation handed to the agent stays sent The offer read its own continuation as not reaching the agent while its dispatch was pending, which also covered one already handed over and still unanswered. When the wait for that answer ended first, the failure it filed read as retryable, and a retry sent a second continuation to an agent that may have acted on the first. Only a continuation still queued, or rejected, is now read as unsent. * 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(cross-version): load the phone row readers without mobile's toolchain Vite transforms a file against its nearest tsconfig, and mobile/tsconfig.json extends expo/tsconfig.base.json, which the root-only cross-version lane never installs. The worktree ps verdict suite imported the current phone row reader from mobile/ directly, so CI failed with TSConfckParseError before any test ran. The harness now imports a copy of the working-tree reader placed under the checkout cache, where the root tsconfig applies, as it already does for the release checkout's copy. Both readers are still the real files. * test(cross-version): keep the checkout path-guard message and justify the copy import's cast * fix(native-chat): a command ends only by its own provider answer or its child's end Stop no longer settles a conversation command. It interrupts it like any turn, and when the provider cannot take that (Codex has not opened the command's turn yet, or Claude refuses the interrupt) it stops the child, whose dead-generation settlement writes the verdict. The pending command now lives on the provider child's own session instead of an adapter-wide map keyed by session, so it dies with the child and nothing has to release it. Claude's /compact is sent under a uuid the slot records, and only a root result naming that input (or naming none) ends it; its outcome is read with the ordinary result reading, so a stopped /compact is a cancellation. * fix(native-chat): a command's settle answers its message before ending its turn The two writes are not one batch. Writing the message's answer first means a crash between them leaves a running command turn, which the stale-turn sweep already settles, instead of an ended turn whose message reads as in flight forever. The settle now writes only while the command turn is still running. * fix(native-chat): "Worked for" counts from the handover, not the send A message held behind /compact, or behind a cold start, used to count the wait as the agent's work, although its row is drawn at the handover. Every handed-over submission's turn, the command's own included, now starts at the handover row's instant, falling back to the send time for a host that recorded none. * test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * test(native-chat): the interrupted create's own retry continues again The merge of main's lease-latch fix replaced that test's retry of the interrupted create, under its own operation id, with a fresh start whose result nothing read. That fresh start passes with the released-reservation continuation deleted, so the case the fix exists for went untested. The retry and its assertion are main's again. * docs(native-chat): three comments that still had views starting agents A start with nothing queued now comes from a command, goal change or rewind; an interrupted compaction left alone would refuse every send, so no agent would ever start to finish it; and a current host raises the unattached read refusal only once quit began, with the attach window belonging to an older host. * 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): a second Sto…
…lyai#23726) * 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. * 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(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(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 * 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(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(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 i…
…tablyai#23829) Third audit wave. The detector looked for production modules exporting three or more symbols that no production file imports — only tests do. That shape is the authoring gate's fourth question failing: a test needing a production seam no caller needs belongs at the real boundary instead. Most hits were detector false positives and were left alone; the scanner misses re-export barrels and dynamic imports, so every module was re-verified with rg before any edit. Where a private predicate's behavior was already covered through the module's real entry point, the duplicate cases are gone and the symbol is module-private again. Where it was NOT covered anywhere else, the test stays — this audit removes tests, it does not author replacements. Production code deleted where tests were its only callers: the superseded `filesystem-directory-listing-limit` module, the unused `format{Hourly,Daily,Adhoc}Version` helpers and their orphaned prerelease identifiers, the dead `filterByAutomationListSearch*` family superseded by `matchAutomationListSearchRowKeys`, and the dead `getAiVaultResumeWorktreeTargetStatus` copy of the live workspace branch. Also drops two call-shape source greps in `relay-sweep-schedule.test.ts` that asserted `index.ts` spells `jitteredSweepIntervalMs(30_000)`; the jitter math has a behavioral owner at the top of the same file. The structural census that counts role-gated vs total `setInterval(` calls stays — an ungated sweep runs in every cell, and nothing else can catch that.
… hosts (stablyai#23780) * fix(mobile): count opening a cached page generation as use; cache six hosts Eviction order was written only when a download committed, so a host opened daily but downloaded long ago was evicted first and each revisit cost a full redownload. An open now refreshes that host's recency inside the store queue. The ceiling rises to six hosts and the update-failure cap to six hosts' worth. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stamp page cache recency by order and never clobber it on open An open stamped wall time, so a backward clock jump made the host in use the oldest entry and the next download evicted it. Recency now stamps past the newest entry. An unreadable or torn index read as empty, so every open rewrote it with one host and demoted the rest; opens now leave it for a commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): wrap the page cache index doc comment to 100 columns Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): skip no-op page cache recency writes and stage the index write Every open rewrote hosts.json even when the host was already the newest, so a single-host user paid a native write per open. Opens now write only when the order changes. The index is written to a sibling and renamed over the old one, so an interrupted write leaves the previous index or none, never a torn file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): keep page cache recency as an ordered host list Eviction needs order only, so hosts.json is now the cache keys least recently used first. That drops the clock, the monotonic stamp, the unreadable-versus- absent split and the staged write: a torn or old-format file reads as empty, the same state a missing one gives. Opens and same-build commits share one touch that writes only when the host is not already last. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…#23789) * fix(mobile): the update-mobile wall opens the exact release When the in-app checker knows the newest release, the update-mobile wall offers "Get Orca <version>" and opens that release's page instead of the generic releases list or a hand-built App Store link. Mounting the wall runs the single-flight, bounded checker once. Dismissal is ignored here. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the update checker out of the page closure The wall now takes the offered release as a prop and opens it through the external-link seam. A shell-only hook runs the checker once per update-mobile wall and supplies the release; MobileWebShellScreen, which no page route reaches, wires it. HostProtocolGate is in every page closure, so it stays unwired pending a ruling. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): platform-split the wall's release offer and throttle its check The hook is the one module whose behaviour differs by platform: the native file runs the checker, the .web.ts sibling offers nothing, so both HostProtocolGate and MobileWebShellScreen wire it the same way and the page closure never carries the checker. A remount checks only when no release is known and the last check is absent or over the retry interval old, since each check is an unauthenticated GitHub call. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let the checker decide whether the wall's check is due The hook's own throttle read lastCheckedAt, which moves only on success, so after a failed lookup every wall remount looked up again, and before preferences loaded a cold-start wall looked up despite a fresh stored result. The checker already owns the cadence: checkIfDue waits for the stored state and checks only past the retry or daily interval. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the wall reads the known release and triggers nothing The started checker's own timer, cold-start and foreground paths already run every due check, so a check requested by the wall could never be due. The hook now only returns the known release; the checker is back to main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): the wall reads its release through useWallAppUpdate The .web.ts sibling alone keeps the page closure clean, so the screen calls the hook itself and the optional prop, the gate wiring and the shell wiring go. HostProtocolGate and MobileWebShellScreen are back to their base content. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…blyai#23744) * feat(claude): pre-trust worktrees Orca creates Claude Code asks "Do you trust this folder?" on first launch in any folder it has not seen, which blocks unattended launches in worktrees Orca itself made. Orca now records where a worktree's content came from when it creates it, and before each Claude launch writes Claude's own folder-trust entry for that worktree's root (never the main checkout) when the new setting is on and the content is the user's repository. Forks, bare commits, folder workspaces and external checkouts keep Claude's prompt. The write takes Claude's lock, never creates or breaks the file, runs on the SSH host itself, and is revoked when the worktree is removed or the setting is turned off. Launches that already pass --dangerously-skip-permissions also skip the trust prompt for that one process only, via CLAUDE_CODE_SANDBOXED=1 on the command. * fix(claude): parse the relay trust request with a schema and ship its search keys * fix(claude): never write a WSL guest's trust into the Windows config A WSL worktree's Claude reads the guest's own config. Two paths still wrote its trust into the Windows host's ~/.claude.json instead: the Claude auth prep's fallback (runtime 'wsl' but the host config dir, when the WSL home cannot be resolved), which wrote a Linux-path key the removal revoke can never delete; and the Agent Teams leader, which passes no auth or distro and wrote a UNC key. Require the guest's own config dir, and treat any WSL worktree path as guest-only. * revert(claude): drop the skip-permissions trust shortcut Pre-trust stays limited to worktrees Orca creates from the user's own repository. The per-launch CLAUDE_CODE_SANDBOXED prefix skipped Claude's trust question in every folder for launches carrying the skip-permissions flag (Orca's default Claude args), including the user's own folders and fork PR worktrees, and the setting could not turn it off. Remove the prefix, the inherited-variable strip that existed only for it, and the Agent Teams leader-to-teammate propagation; restore the tests that pinned the prefixed launch string. * fix(claude): revoke SSH trust in the config file the grant used At spawn the relay resolves Claude's config from the launch env, which carries a CLAUDE_CONFIG_DIR set in Orca's Claude default env. claudeTrust.converge had only the relay's own process env, so removing the worktree or turning the setting off revoked in the default file and the grant outlived the worktree. Send the config-file keys with the request, as the local revoke already uses. * i18n(settings): translate the Claude worktree trust setting * fix(settings): say Claude trust applies when Orca starts Claude The description said Claude skips its trust prompt in any worktree Orca created. Trust is written only when Orca itself starts Claude there, so a `claude` typed by hand in a fresh worktree still asks. Say that, and bring the es/fr/ja/ko/zh translations in line with the new text. * fix(worktrees): treat a base on an Orca-added fork remote as fork content A worktree based on a named ref was always stamped as the repository's own content, so picking the fork remote Orca adds for a pull request (or a local branch tracking it) as the base made a fork's code eligible for Claude trust. At create time, read the repo's `remote.<name>.orca-created` markers and each branch's tracked remote in one `git config` call; a base on such a remote is stamped as a fork's content, and a read failure is not vouched for. Remotes the user added, such as `upstream`, stay first-party. * perf(claude): revoke worktree trust once per config file, not per worktree Turning "Trust worktrees Orca creates for Claude" off read and parsed the whole Claude config once per Orca worktree on the main process. Group the revocations by config file locally and by SSH connection, and make claudeTrust.converge take a batch of requests. * feat(settings): one agent-wide "trust the folder" setting in Settings > Agents Replace the Claude-only worktree trust toggle with a single setting, agentWorkspaceTrustEnabled (on by default; unreleased, so no migration). The row says what it does for every agent: agents Orca starts skip their "trust this folder?" prompt in that worktree or folder, turning it off stops new trust while existing trust stays, and while it is off unattended launches (orchestration workers, automations, the phone) stop at the agent's trust question until someone answers. Translations for es/fr/ja/ko/zh. Also restores the `awaitingUnnamed` chat catalog keys an earlier merge of main dropped from this branch. * feat(agent-trust): pre-trust the workspace for every preset agent at PTY spawn Every Orca-started agent PTY passes through one of the two spawn builders with its declared launchAgent, which survives setup-script wrapping. The builders now call one hook that, for a fresh launch (never a reattach or restored pane) with the setting on, applies the agent's trust preset to the worktree, folder workspace or main checkout it starts in. - One dispatcher, applyAgentWorkspaceTrust(preset, workspacePath, launch context), carries what a writer needs: the final spawn env, the Claude managed-account auth prep, the WSL distro and the SSH connection. - Claude joins the presets on both the claude and claude-agent-teams entries. Its writer stays grant-only in claude-folder-trust-file.ts: the file Claude reads (CLAUDE_CONFIG_DIR / custom-OAuth suffix / legacy .config.json / a WSL guest's own file), Claude's <file>.lock never broken and taken only when a write is due, atomic temp+rename keeping mode and symlinks, never creating the file, NFC + realpath keys. - SSH Claude launches forward the optional claudeFolderTrust spawn field so the relay grants with its own spawn env; old relays ignore it and Claude asks. Other presets keep the SFTP writer. A WSL launch never writes the Windows home: non-Claude presets skip it, Claude writes the guest's file or nothing. - Codex keeps the 20 s deadline its shared config lane needs; every other preset gets 1.5 s. A miss means the agent asks; trust bookkeeping never fails or blocks a launch. Removes the Claude-only machinery this replaces: the eligibility/host/ lifecycle/spawn modules, the persisted creation content-origin field and its classification, revoke-on-removal, the setting-off sweep, the claudeTrust.converge relay method and the Agent Teams leader special case (the leader pane now spawns through the hook with the claude preset). The agent config types move to tui-agent-config-types.ts so the config table stays under the line budget. * refactor(agent-trust): delete the pre-spawn trust writes the spawn hook replaces The spawn hook is now the only owner of agent folder trust, so remove every other writer: - the agentTrust:markTrusted IPC channel, its preload bridge and types, and all renderer callers (agent-trust-preflight and its callers in the background session, work-item direct launch, session continuation, worktree creation, folder workspace composer and session fork); - the main pre-spawn sites: the createdWithAgent preflight in worktree-remote.ts, markLocalWorktreeTrusted/markRemoteWorktreeTrusted and the runtime's markWorkspaceTrustedForAgent family with the markTrusted ports of the runtime create flows; - Codex's own launch-prep and resume-prep trust writes. Each of those launches reaches a spawn builder with launchAgent set, so the hook covers it. This also fixes a live gap: the worktree-remote.ts copy of the preset switch omitted Antigravity, so an agy agent started from a desktop worktree create still asked; the single dispatcher covers it. Trust is also written on the host the PTY actually spawns on, which removes the stablyai#11163 class of writing the wrong host's config. * test(agent-trust): type the spawn-builder trust fixtures and prove the spawn waits for trust The builder test passed untyped args (a string launchAgent) and cast its deps, which failed tc:node. It now builds both spawn states from a fully typed deps fixture and a typed restored pane, with no casts. Adds a case that holds the trust write pending and checks the builder does not finish until it settles, the ordering the deleted renderer and launch-prep tests used to cover. * fix(agent-trust): give SSH trust writes the 20 s deadline again The dispatcher gave every non-Codex preset a 1.5 s budget, including the SSH writers for Cursor, Copilot and Qoder, which make several round trips over the link. Before this PR those writes had 20 s (desktop) or no limit (runtime), so on a slow link an unattended SSH worker would now stop at the agent's trust question. SSH writes get the 20 s deadline back; local non-Codex writers keep the short budget, and Codex keeps 20 s. The relay's Claude grant keeps the short budget: it writes the relay host's own disk and does not cross the link. * fix(agent-trust): never pre-trust a home folder or a filesystem root A folder workspace can be the user's home folder or a disk root. Claude and Copilot let a trusted folder cover every folder under it, so pre-trusting one of those would silently trust everything on the machine for those agents. One check, isHomeOrFilesystemRoot, now refuses them for every preset: the dispatcher checks roots and this machine's homes (including the spawn env's HOME and a cached WSL guest home), the SSH writer checks the remote home it already resolves, and the relay checks its own home. The agent then asks, as it would without Orca. * refactor(agent-trust): drop the Codex launch plumbing that only carried trust The spawn hook replaced the trust writes in Codex launch prep and resume prep, which left the fields that fed them unread: CodexHomeLaunchContext.workspacePath and .launchAgent, the resume prep's workspacePath, and the structured Codex launch input's workspacePath (plus the extra target lookup that produced it). Remove them and their plumbing; unavailableManagedHomePath stays. Also removes test stubs of runtime trust methods this PR deleted, whose not-called assertions could no longer fail, and two comments that still described the old trust preflight. * chore(reliability-gates): point the trust gate at the spawn-time trust tests The agent-session trust gate still listed three test files this PR deleted (the renderer preflight, the Codex launch-prep deadline and the e2e trust completion suites), so check:reliability-gates, which runs in PR CI and in pnpm lint, failed on missing files. Its invariant also described the deleted IPC handler and pre-spawn writers. The gate now covers what replaced them: the spawn builders holding the spawn until trust settles, the fresh-launch and setting gates, the per-preset deadlines, and the home and root refusal. * fix(agent-trust): skip the relay Claude grant for a WSL shell On a Windows SSH host whose pane shell is wsl.exe, Claude runs inside the WSL guest and reads the guest's config. The relay still granted trust in the Windows host's own .claude.json, writing the Windows home for a WSL launch, which the local path never does. The relay now skips the grant there, so that Claude asks, as a local WSL launch does when Orca cannot reach the guest file. * perf(agent-trust): only agent launches wait on the trust hook Both spawn builders awaited the trust hook on every spawn, including plain shells, reattaches and agents without a preset. Awaiting even a resolved promise adds microtask ticks ahead of the pane-spawn reservation check, and this handler already keeps non-Codex spawns off an await because an extra tick reorders those reservation races. The hook now returns null when there is nothing to write, and the builders await only a real trust write. * test(agent-trust): keep the home and root cases off any real Claude config The home and root cases ran the real Claude writer with the test process's env, so a regression in the guard would have written trust for the home folder and / into whatever Claude config that env named. They now point CLAUDE_CONFIG_DIR at a folder that does not exist, and the writer never creates a config. * test(runtime): drop needless casts from the launch-host test The renamed launch-host test kept three `as never` casts on launch options that already match launchAgentTerminal's parameter type. The changed-lines casting gate reads the renamed file as new and failed on them. * test(agent-trust): type the Claude grant mock with the real writer's signature The mock took an unknown target, so installing the real writer as its implementation would not typecheck under strict function types. * fix(codex): drop the launch context the trust move left unread in local spawn env * fix(agent-trust): queue Claude grants per config file so a launch burst keeps them all Concurrent grants in one process retried Claude's file lock in lockstep, so each retry round admitted about one winner. Starting 12 Claude agents at once left 6 of them at the trust question with nothing logged. Grants for one config file now queue in-process; only Claude's own writes contend for the lock. The relay shares the writer, so bursts of SSH launches are covered too. * fix(agent-trust): never pre-trust a folder above a home either The guard refused only an exact home or a filesystem root. A folder workspace at /Users, /home or C:\Users was still pre-trusted, and Claude walks up parent folders for a non-git folder, so every non-git folder in the user's home became trusted. The guard now also refuses any folder that contains a home, on every host, and is renamed to say what it decides. * perf(agent-trust): skip the SSH round trips for Antigravity, which has no remote writer Every Antigravity launch over SSH now reaches the remote trust writer, which resolved the remote home and realpath'd the workspace over the link before writing nothing (the known remote gap). That delayed each launch by two SSH round trips, and up to the 20 s deadline on a stalled link. It now returns first. * fix(settings): keep the hidden folder trust row out of web-client settings search The paired web client hides the host-only "Trust the folder" row, but settings search still listed it, so searching "trust" opened the Agents pane with no matching row. Its search entry is now filtered the same way as Agent Awake. * fix(agent-trust): a failing breadth guard skips trust instead of failing the spawn * docs(qoder): New Tab now pre-trusts through the agent-wide spawn hook * fix(agent-trust): never pre-trust a home reached through a symlink Every trust writer stores the workspace's resolved path, but the breadth guard compared only the path as given. A folder workspace that is a symlink to the home folder (or a real home picked while HOME names a symlinked one, as on distros that link /home to /var/home) passed the guard, and Claude, Copilot and Cursor then trusted the home itself. The local dispatcher and the relay now compare given and resolved forms of both the workspace and each home. The SSH writer resolves the remote home alongside the workspace, in parallel, so it adds no round trip. Local non-Claude WSL launches still skip before any filesystem call. * fix(relay): a failing breadth guard skips Claude trust instead of failing the SSH spawn The relay ran its home/root guard and homedir() before its catch, so a throw there rejected the relay's terminal spawn. Same fix as the main dispatcher's: the whole grant, guard included, is best-effort. * fix(agent-trust): guard the path each writer stores, not the path Orca was asked to trust The breadth guard checked the launch's workspace while each writer stored a transformed path, so every new transformation opened a hole. Codex stores a linked worktree's main checkout: with a git repo rooted at the home, a Codex launch in one of its worktrees wrote trust for the whole home. One relay-safe host module now computes the stored path (Codex's main-checkout hop, then given and resolved forms of it and of each home), refuses a root, a home or a folder above one, and only then writes. Main uses it for local and WSL launches and the relay for Claude. An unknown home writes nothing, and the WSL home cache is keyed case-insensitively by distro. * fix(ssh): the relay writes every preset's trust on the SSH host itself Codex, Cursor, Copilot and Qoder trust over SSH was written from the desktop over SFTP: four or five round trips per launch, so it needed a 20 s deadline that outlasted the 8 s draft paste, the 10 s phone wait and the 15 s web-client create. It also skipped Claude's atomic rename, ignored CODEX_HOME, and stored the worktree where local Codex stores the main checkout. The unreleased `claudeFolderTrust` spawn field becomes `agentWorkspaceTrust`, sent for every preset. The relay derives the preset from the `launchAgent` it already receives and runs the same host writer main uses, on its own disk, within 1.5 s and with no extra round trip. Antigravity still returns early on the relay (its writer is unverified on SSH hosts), a WSL shell still skips, and any throw means the agent asks. Deleted: the SFTP preset writer, the remote Qoder writer, the SSH deadline clause and the desktop-side SSH root pre-check. * test(e2e): keep CLAUDE_CONFIG_DIR out of isolated Electron launches The spawn hook now writes Claude folder trust into the config CLAUDE_CONFIG_DIR names, so an e2e run started from a shell that sets it could add trust entries to the developer's real Claude config. Also drops a stale comment that still named Codex launch prep as the trust owner. * fix(agent-trust): guard Claude's resolve() form of the workspace too Claude's writer stores both resolve(path) and the realpath. The breadth guard compared only the given path and its realpath, so a workspace path that does not exist and climbs back with `..` (for example <home>/missing/..) passed the guard while Claude stored a key for the home itself. The guard now also compares resolve(path), so it sees every form a writer stores. * test(relay): pty.spawn writes agent trust before the agent's process starts Nothing exercised the relay handler's call into the trust writer, so removing that call, or no longer awaiting it, left every suite green while SSH launches silently stopped pre-trusting. The new case holds the trust call pending and checks the spawn waits for it, and that the call gets the request, the declared agent and the final spawn env. The reliability gate lists the new suite and records the resolve() form the breadth guard now compares. * fix(agent-trust): refuse a home only for agents that inherit trust from it The home and root refusal applied to every preset, so Codex, Cursor and Antigravity started asking in a home folder workspace, where they did not before. Only Claude, Copilot and Qoder let trust on a folder cover the folders below it; Codex matches its start folder or that folder's repo root, Antigravity the exact folder, and Cursor itself never inherits from a home, a folder above one or a shallow path. The refusal now reads a per-preset table in the host module, so the local and relay writers share the rule. * fix(agent-trust): trust Codex at the folder it starts in, as before Before this PR, Codex launch prep trusted the spawn's start folder. The spawn hook trusted only the workspace root and skipped terminals with no workspace, so Codex began asking in a floating terminal and in a subfolder of a non-git folder workspace: its lookup checks the start folder, then that folder's repo root, and a plain folder above it is neither. The hook now passes the resolved start folder for presets marked as keyed by it (Codex only), falling back to the workspace root. * fix(agent-trust): pre-trust a structured Codex chat's folder, as before Before this PR, creating a structured (native) Codex chat pre-wrote Codex trust for its folder through launch preparation. The PR removed that write and routed trust through the PTY spawn builders, which a structured chat never passes. Codex's app-server trusts the folder itself only when the chat's permissions can write it, so a read-only chat started running untrusted and ignored the project's .codex config. Creating the chat now calls the same dispatcher, behind the same setting, before launch prep. * fix(settings): plainer folder trust setting text
… unfinished clear holds nothing (stablyai#23940) stablyai#23726 and stablyai#23524 merged minutes apart. stablyai#23726's queue gate takes its "blocked" answer from the shared send check, which stablyai#23524 changed: a /clear that never committed changed nothing the chat reads, so it refuses no send. Three stablyai#23726 tests still seeded a "prepared" /clear record and expected it to hold the queue, and the /clear carry passed a possibly-undefined replacement id where stablyai#23726's helper takes a string, so main failed its unit tests and tc:node. The gate code already follows the new rule; the tests now say it: a /clear an older build left prepared holds no draft and Send-now sends it, and a send made while a /clear is starting its replacement is still told to wait. The carry reads the replacement id from the ids it already checked.
…chat feed that replaced it (stablyai#22939) * fix(mobile): don't unsubscribe a terminal stream that already ended * fix(mobile): keep the stream registry under the line cap and expect no error after a streamed end The streamed `end` now closes a direct stream, so a later reply for that id is unrouted instead of reaching the listener as an error; the subscription recording test expected the old error. Inline the error wrapper so the registry fits the 300-line cap, and shorten the comments. * fix(mobile): reopen a native chat stream the host ended while the screen still shows it * fix(mobile): only the focused screen takes back a host-ended chat feed, and a reopen keeps loaded history * fix(mobile): give each native chat subscription its own token, and reopen with the first page * test(mobile): pin the reopened chat subscribe params without a cast * fix(mobile): show a host-ended chat feed as an error instead of reopening it With a token per subscription, the desktop ends a phone chat feed only when the phone closes it or the socket drops: no client sends the connection-wide chat unsubscribe, a socket close delivers no end, and every desktop with mobile chat keys feeds by the client's token. The reopen-with-backoff layer and the history-keeping reopen merge guarded a trigger that no longer exists, so they are removed. An end that still reaches the screen settles the feed as an error, so a dead feed is never shown as live; leaving and re-entering the chat subscribes again with a fresh token. * test(mobile): re-record the RPC goldens for the per-subscription chat token Each nativeChat.subscribe now sends its own token, so the native chat scenarios expect `claude:session-1:<id>` in the subscribe params, and the four native chat recordings (native-chat-page-earlier and the session.native-chat-page matrix) record that token in their subscribe and unsubscribe payloads. Every other golden moved only its `baseline` header, repinned to the commit recorded from. * test(mobile): repin RPC recordings to the merge with main and re-record Merging main moved the product tree the recordings are pinned to, so the baseline is repinned to the merge commit and the whole corpus re-recorded. Only the four native chat recordings change content, because each nativeChat.subscribe now carries a per-subscription token; every other recording moves only its baseline header. * test(mobile): repin RPC recordings to the merge with main and re-record Merging main brought stablyai#22762's recordings, stablyai#23757's repin and stablyai#23720's recorder change, so the corpus is repinned to the merge commit, the last commit to touch a recorded path, and re-recorded whole. Against main, 786 recordings move only their baseline header and 4 native chat recordings change content: their nativeChat.subscribe and nativeChat.unsubscribe carry the per-subscription token instead of claude:session-1, which also moves their scenarioSha256.
… owners (stablyai#23941) Fourth audit wave, cut short by a session restart, so this lands the verified subset rather than the full batch. Removes private-predicate cases whose behavior is already covered through the module's real entry point, and de-exports the seams they reached for. Also drops three whole files whose every case was a duplicate or a call-shape grep. The source-grep vein is close to exhausted. One auditor reviewed 15 remaining flagged files and deleted nothing: what is left is mostly legitimate architectural ratchets that no type checker and no behavioral test can reach — AST fences banning `as`/`any` in an RPC operation region, discovered-vs-listed set equality over subscription sites, count ceilings on unchecked reply readers, and assertions on generated WebView bundles (no CDN URL, no `</script` tokenizer escape, parses at the Chrome 74 floor). Those stay.
…3945) Fifth audit wave, renderer-scoped. Same shape as stablyai#23941: delete private-predicate cases whose behavior is already asserted through the module's real entry point, then make the symbol module-private again. Two whole files went as provider-local replays: `terminal-paste-payload-metadata` (same corpus, same inputs, same expectations as `lib/paste-payload-metadata`, just renamed symbols) and `repro-8784-ghe-avatar-fallback` (every case now asserted through the rendered component in `github-user-avatar.test.tsx`). Dead production code removed where tests were its only callers: `resolveImeModifierGesture`, `isCodexTerminalStartupCommand` (its codex branches are covered by `isKnownTuiAgentTerminalStartupCommand`), `reconcileSelectionKeys` (superseded by `reconcileSourceControlSelectionState`, which the hook calls), and `chatFontScaleShortcutLabels` (nothing renders those labels). Roughly three quarters of the flagged modules were detector false positives and were left untouched; the scanner cannot see re-export barrels, `.web.ts` platform variants, build-time overrides, or `*ForTests` inspection seams. `accumulateWorkItemPages` is deliberately NOT deleted despite having only test callers. It, the dead `filesystem-directory-listing-limit` module, and `resolveImeModifierGesture` all landed unwired in the same refactor (stablyai#16177), and unwired page accumulation is more likely a lost wiring step than dead weight. Deleting it would erase that evidence.
…ed by one process (stablyai#23613) * feat(native-chat): one structured-chat journal database per host, owned by one process Every structured chat on a state directory now lives in one SQLite file, agent-session-journal.db, opened once by the process holding agent-session-journal.owner: an empty SQLite file whose held BEGIN EXCLUSIVE is a kernel byte-range lock, refused while another process holds it and released when the holder dies. - Stores own no connection: the per-chat handle, its close contract and the close-retry registry are gone; closing a conversation drains its writes, and the one connection closes last at teardown. - The owner lock is taken at runtime start, before orca-runtime.json is written; a process that does not own the chats is not published and refuses every structured request with journalUnavailable and words that say what to do. It retries the lock with backoff and runs the full install once it holds it. - A journal that will not open fails the host install: every chat says "Unable to load this chat." (journalCorrupt), and nothing is renamed, deleted or rebuilt. A newer build's database is refused and left byte-identical. - An append is one INSERT. The listing status is a column, written after the rows it describes and keyed by (epoch, sequence). - A per-chat journal from an earlier build is copied in verbatim (epoch UUID and every sequence) on that chat's first open, and its directory is retired only after the copy commits. - auto_vacuum = INCREMENTAL, with freed pages handed back in bounded steps after every delete. * perf(native-chat): key journal rows by block so one chat's rows sit together Each chat's live epoch owns a block of row ids, block * 2^32 + seq, so a chat's rows share leaf pages with nobody else's, a replay is one range scan, and replacing or rewinding a chat deletes one contiguous range. Measured on the largest real chat (61 MB) beside 19 interleaved peers: 39 ms and 7.5 MB of WAL, against 214 ms and 102 MB for a (session_id, epoch, seq) key. - Ids are computed in Number arithmetic, never bitwise. A sequence is refused outside [1, 2^32) and a block at 2^21, which keeps every id below 2^53. - A replace, roll or import allocates a fresh block, moves the chat's pointer, and deletes the old block in the same transaction, so no orphan block exists. - The listing status write moves into its own writer beside the column. * feat(native-chat): copy a chat's per-chat journal again when an older Orca wrote it after a downgrade A per-chat journal.db that reappears after its chat was copied in is the newer history: an older build, run after a downgrade, attached the chat and wrote it. - journal_imports records the (epoch, tip) each chat was copied from, in the copy's own transaction. A file already copied is never copied again, across any number of restarts after a failed rename; a file that differs always is. - Newest writer wins, per chat, with a row saying the chat was continued in an older version of Orca. When both builds wrote past the recorded tip under one epoch, the copy takes a fresh epoch, so readers reset instead of skipping rows. - Each copied directory retires to its own .imported-<epoch8>-<ms> name, so a second downgrade and re-upgrade never collides with the first. * test(native-chat): fixture deps match the host journal database shape Attach-flow and reconcile-attach fixtures stop passing a journal database those inputs do not take, and host and restore fixtures pass the one they now require instead of the removed journal root. * test(native-chat): state why the runtime-state fixtures' existing casts are safe * fix(native-chat): start up normally when this process cannot open the chat journal A process refused the chat journal, because another Orca owns it or because its own journal will not open, failed startup restoration: the window booted in degraded no-save mode and a paired phone could not list any tabs. Startup restoration now treats the refusal structured requests are getting as having no structured host; terminals, tabs and saving go on, structured requests are still refused by the gate, and the install is retried on the next one. Any other install error fails startup as before. * test(native-chat): name the owner-lock sweep test after the two sweeps it runs * fix(native-chat): session history and terminal resume work while chats are refused Session history (listing and preparing a resume) and a terminal typing a resume command only check whether a structured chat owns a provider session. In a process refused the chat journal they failed outright. They now take the refusal chats are getting as having no structured host, the same treatment startup restoration gets, through one shared helper; any other install failure still fails them. Chat requests keep the gate's refusal. * test(native-chat): the first-work rename's fake journal saves the listing status * fix(native-chat): open a chat whose per-chat journal file never got its schema A crash between creating a chat's journal.db and creating its tables left an empty or schema-less file. Each chat used to open that file as an empty chat; the importer instead refused the open as "try again" forever. A file with no journal_sessions table is now read as never written, the same as one with no rows. A file that is not a database, or whose read fails, is still refused. * fix(native-chat): let the event loop run between chats during startup restore Opening a chat's journal is synchronous SQLite now that no per-chat directory is created first, so the restore of every visible chat ran as one main-thread task. Each chat now waits for a macrotask before it opens. * fix(native-chat): import a per-chat journal in bounded batches The one-time copy of an earlier build's per-chat journal ran as one transaction, which blocked the main thread for 650 ms on the largest chat. Rows now copy 512 at a time, each batch its own transaction, yielding to the event loop between batches. The rows go into a block journal_import_blocks reserves, which no reader follows and no other chat is allocated; the last batch publishes the chat's pointer, repair marker and import marker together and releases the reservation. A copy that stops midway leaves only that block, which the next open clears and copies again. Two opens of one chat import one after the other. * fix(native-chat): refuse chats when the owner lock file cannot be opened A lock file that is not a database, or cannot be opened, made the claim throw before any refusal was recorded, so startup restoration failed on every launch. The claim now sits in the same try as the database open and records the same typed refusal. * fix(native-chat): finish reclaiming pages a delete frees during a running pass A reclaim pass ended as soon as the freelist stopped shrinking between steps, so a second delete that freed more than one step's worth mid-pass ended it early and left those pages on the freelist. A pass now ends only when a step itself frees nothing, or the freelist is empty. * test(native-chat): desktop session history is served while chats are refused * fix(native-chat): a send to a chat holding a newer Orca's rows says to update A chat opened read-only because a newer Orca wrote rows to it answered a send with the generic write failure. It now refuses the way a database a newer Orca wrote does, with the same reason and words. * fix(native-chat): keep chat tabs while this process cannot list its chats A process whose chats another Orca owns, or whose chat journal will not open, has no structured host. Its session-tabs inventory still answered, with no chat rows, and the renderer read that as "every chat was closed": it removed the restored chat tabs and the next session save persisted their placement away. The inventory now says `agentSessionsUnverifiable` when the last tab restore ran with chats on disk but no host to list them. The flag is set and cleared at the per-client projection point beside the client-hosted page hold, and the restore is memoised only once a host answered, so a later lock takeover or journal open republishes the chats and clears it. The renderer keeps agent-session tabs, and keeps cancellation tombstones, against an inventory that does not affirm its chat set. * fix(native-chat): say chats are open in another Orca, with this process's way past it A process refused because another Orca owns the profile's chats sent the generic `journalUnavailable` reason, so current desktop and phone surfaces said "couldn't open this chat's history right now. Try again." — a step that never helps while the other Orca runs. The refusal now names its own reason, `journalOwnedElsewhere`, with the refused process's kind (dev desktop, packaged, orcad) as a fact. Each kind gets its own step: quit the other Orca, or give this one its own profile (ORCA_DEV_USER_DATA_PATH) or data folder (ORCA_USER_DATA). The sentences are added to the shared notice copy, the desktop catalogs in all six locales, and the boot catalog. A client that predates the reason reads it as none and keeps the code's words; an unknown kind reads as the packaged app's step. The `message` released clients print is unchanged. Which requests refuse does not change. * fix(native-chat): restore chats on taking ownership, without a list to ask A refused startup kept its hostless result, so after the owner quit this process never installed a host, never reconciled restart leases, and kept telling clients it could not list its chats until a desktop chat request. Taking the lock now reruns startup restoration once and pushes the chats. * test(native-chat): a navigation reply says chats are unverifiable while refused * fix(native-chat): retry a refused owner lock at most every 5 seconds The lock frees as its holder exits, but a refused process only learns that on its next retry, and the 30 s cap left a second Orca refusing chats for up to half a minute after the owner quit. One retry is an open and BEGIN EXCLUSIVE on an empty file. * fix(native-chat): install before deciding whether a takeover must republish chats A list that landed on the refused startup after the lock was taken finished after the takeover had already checked, so nothing republished. The takeover now installs first, waits for any restore in flight, and restores only then; the restore that clears "cannot tell" pushes the frames itself, so a list that heals the inventory first reaches subscribers too. * fix(native-chat): no takeover lands a host after the runtime stop Quitting cancelled a refused claim's retry only at its end, so a retry firing during the stop's awaits took the lock and installed a host the stop never tore down, and the lock was then released under an open journal. The stop now cancels the retry first, keeping the refusal, and repeats its teardown while an install that began during it (a takeover already under way) is pending, so no journal connection outlives the lock. * fix(native-chat): show a thrown refusal in its own words, not its code A refusal the host throws reaches the client as an RPC error whose message is the bare code; its reason and facts ride only in the error's data, which no client read. The chat pane's status line therefore printed agent_session_journal_unreadable, a send took the bare "not sent" path, and other writes said the outcome was unconfirmed. One shared reader, agentSessionThrownRefusal, now reads the refusal from the error data. A failed history read shows the refusal's read-history words, a send keeps the refusal behind its Retry exactly as a returned refusal does, and the other writes (desktop and phone) name the refusal instead of doubting the outcome. The phone's read failure goes through the same reader. * fix(native-chat): log a failed journal open once per distinct failure Every chat request retries a journal open that failed, which is intended, but each retry also logged the failure with its full stack: a junk database file logged the same "file is not a database" error 189 times in a minute. The open now logs a failure only when its code and message differ from the last one logged, and forgets it once an open succeeds. The retry is unchanged. The open moves to its own module beside the runtime, which had no room left. * fix(native-chat): restore lists a chat from its per-chat file and copies it on first use Startup restore opened every restored chat, and that open copied the chat's per-chat file into the host database, so the first boot after an upgrade paid the whole one-time copy before the chat list appeared. A restore open now reads a chat that is still in its per-chat file straight from that file, read-only, with the importer's own reader, and closes the file before moving on. That read drives the listing, the status row and the restart offer, as it did when every chat had its own file. The copy becomes owed work on the chat's write queue: it runs before the chat's first write, and a reader that reaches the chat awaits it. A chat the host already holds, or that was copied before, still opens through the import and its reimport rules, and so does a file whose read needs a repair written. * fix(native-chat): no host stays registered after a stop an install spanned Each teardown pass clears the registered host before it awaits an install in flight, and that install registers its host when it finishes. The pass then tore the host down but left it registered, so a request after the stop was served by a host whose journal was closed. The stop now clears the slot once its passes are done. * fix(native-chat): checkpoint the journal with a full flush on macOS synchronous = FULL fsyncs each commit, but macOS fsync leaves the drive cache unflushed, so FULL alone does not survive a power loss there. With checkpoint_fullfsync, each checkpoint uses F_FULLFSYNC; elsewhere it is a no-op. The comment that said FULL alone was enough is corrected. * fix(native-chat): delete a per-chat journal once its copy verifies An imported chat's per-chat file was kept under an `.imported-*` name, which doubled the disk its history takes. The copy now reads back from the host database before it is published: its items, submissions, epoch and tip must match the file's. Only then does one transaction publish the chat with its import marker, and the file and its WAL files are deleted, the directory too when nothing else is in it (a pre-SQLite transcript there is kept). A copy that does not match is never published: the file stays, the chat is refused as unreadable ("Unable to load this chat."), and the mismatch is logged once. A file left behind by a failed delete or a crash matches the marker, so the next open deletes it rather than copying it again; a file an older build wrote after a downgrade still differs, and is still copied again. * fix(native-chat): verify an imported chat a batch at a time The check that a copied chat reads back as its per-chat file folded both whole, each in one synchronous task: over half a second on the largest chat. Both reads now go a batch at a time between turns of the event loop, like the copy itself, and count rows as well, so a copy that lost a row with no item in it is caught too. * fix(native-chat): restore reads a chat's per-chat file a batch at a time Restore folded a chat still in its per-chat file in one task, so the largest chat's file held the main thread for about half a second at startup. The fold now takes the file a batch of rows per turn of the event loop, into the same fold a replay uses, and nothing reads it before it is done. The file is still closed before restore moves on. * fix(native-chat): end a per-chat copy on a turn of its own A chat's first open ran the copy's last steps (the verified publish and the per-chat file delete) and the replay of what was copied in one task. The copy now yields before it returns, so the replay, which every open runs, is a task of its own. * fix(native-chat): commit a per-chat copy's batches without an fsync each Each 512-row batch of a chat's first-use copy committed under synchronous = FULL, so a large chat paid one fsync per batch, about a quarter of its first open. The batches now commit under NORMAL, set and restored in the batch's own task so no other chat's commit runs under it. The publish that makes the copy visible still commits under FULL, and under WAL that sync makes every earlier batch durable with it. A crash before it leaves only the unpublished block, which the next open clears and copies again. * fix(native-chat): roll back a chat journal transaction whose COMMIT fails The shared connection's transaction rolled back only when its body threw. A COMMIT that failed left the transaction open, so every later write, for any chat, failed with "cannot start a transaction within a transaction", and reads saw rows that never committed. Under the unsynced copy the failure also tried to restore the sync level inside the open transaction, which SQLite refuses, so the caller got that error instead of the COMMIT's. One transaction helper now covers the body and the COMMIT, rolls back whatever transaction survives, and rethrows the original error. Schema creation uses it too. If that ROLLBACK fails as well, the connection is marked stranded: each later use retries the ROLLBACK, and until one goes through every chat gets the same "history unavailable, try again" refusal a journal that will not open gives. The rollback that frees it also restores the FULL sync level. * fix(native-chat): keep the chat journal connection until its close succeeds Closing the journal dropped its connection handle before closing it. A close that failed left the database reporting itself closed with the connection still open, so the stop that retried the teardown found nothing to close and released the owner lock over a live connection. The handle is now dropped only once the close succeeds. A failed close keeps the runtime pending and the lock held, and the next stop closes that same connection before it releases the lock. * fix(native-chat): publish the runtime only once it holds the chat journal lock When this process could not open the owner lock file at all (a permission error, or a file that is not a database), the runtime counted that as owning the chats and wrote orca-runtime.json. That overwrote the real owner's entry, so the CLI was sent to a process that cannot serve its chats. A claim that throws is now refused like one another process holds: the runtime starts but does not publish, the claim's existing retry keeps asking for the lock, and discovery publishes once the retry takes it. Chats still get the refusal for the failure itself, and startup restoration reruns on the takeover the same way it does after another owner quits. A sole process whose lock file never opens is not found by the CLI until it does. * fix(native-chat): keep a chat's history when an older build started it over The first copy deletes a chat's per-chat file, so an older build run after a downgrade finds no file and starts the chat from nothing. On the re-upgrade that fresh file was copied in as the newer history, replacing everything the shared database held for the chat, and then deleted. A file whose epoch is not the one last copied and that opens with `session_created` is now kept: neither copied nor deleted, and the chat keeps the history it has. A file that carried the copied epoch on is still copied again, as before. * test(native-chat): pin which chats startup restore copies Restore copies a chat still in its per-chat file only when restore itself has to write to it: settling what the last run left open, here a running tool call or a send handed over and never answered. Every other restored chat stays in its file until its first use. * test(native-chat): pin the copy wait on a read that opens a chat restore opened A read queued behind restore's open of the same chat reaches the conversation through its own open rather than the listing. It must still wait for the owed copy, or it reads the chat before its history is in the one database. * fix(native-chat): record a set-aside per-chat file so no later open reads it Setting aside a file an older build started over is decided once and kept in the new `journal_set_aside` table (schema 2, additive), with the file's epoch and tip as they were. Every later open of the chat skips the file without opening it, across restarts and after the older build writes more to it: anything written there grows from that build's own start, never from this build's history. The best-effort delete moves beside the per-chat file reader. * fix(native-chat): set aside any per-chat file at an epoch this build never copied A chat's per-chat file is deleted once its copy verifies, so a file that reappears at another epoch was never this build's history, whatever its first row says: an older build started the chat over, possibly rewinding it after (`handle_forked`), or rolled the epoch of a file whose delete had failed. Copying any of them would replace everything the chat holds, so each is set aside. Only a file still at the copied epoch is copied again (it grew) or deleted (it did not). The first-row check is gone. * fix(native-chat): copy a reappearing per-chat file again only while this build has not written past the copy A per-chat file that an older build carried on under the copied epoch was copied again even when this build had also written to the chat since the copy, or had rolled its epoch. The second copy replaced the chat's block, so what was sent in this build after the copy was gone for good. Now the file is copied again only when the chat still stands exactly as it was copied: the same epoch and tip the import marker recorded. Otherwise it is set aside like any other file that is not this build's history, left on disk untouched and recorded so no later open reads it. A second copy therefore never replaces rows this build wrote, keeps the file's own epoch, and the fresh-epoch rewrite goes away. The row it adds now says the history includes what the older version recorded, not that anything was replaced. * test(native-chat): pin that a chat founded here keeps its history, and the v1 schema upgrade A chat this build founded has a pointer and no import marker, so a per-chat file an older build later starts for it is set aside. Nothing pinned that half of the rule: letting such a chat be copied again replaced its history and every test still passed. A second test pins that a database written at schema version 1 upgrades in place, gaining the set-aside table and keeping its import markers. * test(native-chat): drop a lost copied row by patching the source, not wrapping it * chore(mobile): restore the mobile lockfile to main's * fix(native-chat): pass a classified journal refusal through a send or Stop unchanged * fix(native-chat): refuse a read whose owed copy fails as a failed open does * test(native-chat): measure only the replace's WAL in the block-key case Opening the chats starts a free-page pass that waits one event-loop turn, and the seed never yields one, so that pass was still pending when the replace committed. It woke during the async stat and reclaimed the pages the replace freed, adding ~500 KB of WAL whenever the stat lost the race (Linux CI). Drain that pass before measuring and stub the replace's own. * test(native-chat): the RPC fixture's status journal can save its listing status The status feed now hands every projection to the journal, which decides whether it is worth saving. * test(native-chat): state why the RPC fixture's status journal cast is safe * fix(native-chat): refuse a per-chat copy whose rows differ from the file, not only its counts * fix(bench): build the replay benchmark's baseline arm from the base tree and release its handles on failure * fix(native-chat): retry a failed listing status save on the next read of a cached status * refactor(native-chat): drop the chat journal owner lock; the process instance lock already guards the profile The journal carried its own exclusive lock, with a retry loop, an in-process takeover, lock-gated runtime discovery and a "chats are open in another Orca" refusal. Every shipped process kind (packaged desktop, serve mode, orcad) already refuses a second instance on one profile before the journal opens, so the lock only ever mattered for dev desktops, which the next commit covers at the process level instead. The host now opens its one journal connection at install with no lock. What a sole process whose journal will not open needs stays: the install refusal recorded for the gate, the no-host startup path, and the unverifiable chat inventory, now in structured-agent-session-host-refusal.ts. The unreleased journalOwnedElsewhere reason, its processKind fact and their copy are removed. * fix(startup): dev desktops take the single-instance lock, and a second one says why it quit Dev skipped Electron's single-instance lock so parallel `pnpm dev` runs from several worktrees would not quit silently, but two dev processes on the default orca-dev profile then write the same stores at once. Dev now takes the lock like packaged builds: a second launch on the same profile focuses the first window and exits with code 3, printing one stderr line that names the taken profile and how to run another copy (ORCA_DEV_USER_DATA_PATH). Serve mode, the macOS diagnostic bypass and the E2E harness are unchanged: an E2E launch still skips the lock unless it sets ORCA_E2E_ENFORCE_SINGLE_INSTANCE_LOCK=1. * refactor(native-chat): key journal rows by chat, epoch and sequence Rows in the host's journal database are now addressed by the chat's own identity, with `(session_id, epoch, seq)` as the primary key, the same shape each per-chat file already used. The block-keyed layout goes with everything built on it: the block column and its allocator, the 2^21 block ceiling, and the import's reserved block table. A first-use copy writes its rows under the file's epoch, which the chat's pointer does not name until the verified copy publishes it, so no reader sees a half-copied chat. A try that stopped midway leaves only rows no pointer names, and the next try deletes them before it copies again. Replace, rollover and repair delete by (chat, epoch). This build's history always wins: once a chat was copied or founded here, any per-chat file that reappears is set aside, and the same-epoch copy again after a downgrade is removed. The bounded free-page reclaim after every delete is dropped; `auto_vacuum = INCREMENTAL` stays at file creation, so a later periodic reclaim can still be added. Session search keeps its own step. The schema moves to version 3. Versions 1 and 2 were written only by unreleased builds of this change and are refused as found, not migrated. * fix(native-chat): open a chat journal a newer Orca wrote read-only instead of refusing it After a downgrade, the host's journal database carries a newer user_version. It was refused outright, so every chat's history disappeared. It now opens on a read-only connection, as the per-chat journals did: each chat shows what this build can read, from the database or a per-chat file never copied in, and every write is refused with "Chats were saved by a newer Orca. Update Orca to keep using them." Nothing is written, copied, repaired or founded, and the file stays byte-identical. A table the newer schema changed reads as the same read-only refusal, not damage. * refactor(native-chat): leave the saved listing status to the change that reads it Nothing in this change reads the per-chat listing status column: it was a stored copy of a fact the status feed derives, written after every turn end and cleared on every epoch change. The status_json / status_seq columns, their writer, the saved-status type, the status feed's save and its retry on a cached projection all go, with their tests. The change that lists chats from a saved status adds the column back beside its reader. * fix(native-chat): a chat saved by a newer Orca says to update Orca, not to try again When a newer Orca wrote the chat journal, this build opens it read-only. A send or a Stop was refused with the reason `journalUnavailable`, so today's desktop and phone clients chose the words for an open that can clear: "Orca couldn't open this chat's history right now. Try again." Retrying never cleared it; only updating Orca does. The refusal now names its own reason, `journalWrittenByNewerOrca`, whose words are "Chats were saved by a newer Orca. Update Orca to keep using them." A read refused the same way names it too. An older client does not know the reason, drops it, and falls back to the code's words ("Orca couldn't read this chat's saved history."), and released clients still print the message. * fix(native-chat): a chat journal from an unreleased build reads as unusable, not as retryable A chat journal database stamped with schema 1 or 2 was written only by unreleased development builds of this change. Opening it threw a plain error, which every chat reported as "Orca couldn't open this chat's history right now. Try again." Retrying never cleared it. It now throws a named error that is classified as unusable, so every chat says "Unable to load this chat." The one log line names the file, says an unreleased development build wrote it, and says to move it aside. Nothing migrates or renames it. * docs(native-chat): drop the second-Orca-owns-the-chats case from three comments The chat-only owner lock is gone, so only a chat journal that will not open leaves a runtime unable to list its chats. * docs(native-chat): correct three chat-journal comments the redesign left behind A per-chat file left without its WAL is set aside, not copied again; nothing runs an incremental vacuum yet, so the auto_vacuum mode is kept for a later pass; and the idle sweep drops a chat's in-memory fold, since a chat holds no journal connection. * refactor(native-chat): stop exporting chat-journal names nothing imports Each is used only inside its own module now; the teardown's export served a deleted test. * test(native-chat): name the version-0 test for what it covers, and check every journal table The test named 'migrates an older user_version forward' covers only a version-0 file that already has its tables; versions 1 and 2 are refused. The table test now also checks journal_imports and journal_set_aside. * fix(startup): a second dev launch's exit line no longer claims it focused a window The running dev instance may be a background launch or a server, which show no window. The line now says only that this launch passed its request to that instance. * fix(native-chat): a failed structured-chat install closes the journal connection it opened The install opened the chat journal database and closed it only if the record store then failed to open. A later failure, such as the model catalog wiring or the host constructor, left the connection open, and the next install opened a second one in the same process. Every failure after the open now closes it.
…rnals sweep (stablyai#23949) Sixth and final wave over the modules that export symbols only tests import. Deletes private-predicate cases whose behavior is already asserted through the module's real entry point, then makes the symbol private again. Also removes three distinct junk shapes the earlier detectors missed: - a self-comparison whose expected empty row was produced by the helper under test (`worktree-palette-search`), now a literal; - expected values computed by a sibling helper rather than asserted (`terminal-theme`), now read through the production `getBuiltinTheme`; - a negative control that cannot fail — `expect('json' in jsonlMonarchLanguage) .toBe(false)`, where `IMonarchLanguage` has no such key, so it guarded nothing while appearing to guard "does not attach the JSON language service". Dead production code removed where tests were its only callers: `refreshWindowsTerminalCapabilities` (a one-line alias for `loadWindowsTerminalCapabilities({force: true})`), `readSpoolRecords`, `buildAgentPromptSubmitBytes`, and `getCommitMessageModelCapability`. About 70% of everything this detector flagged across the whole vein was a false positive, so most modules were left untouched. Bounds consumed as test input, `*ForTests` seams, non-hook cores of `useSyncExternalStore` hooks, and value-position registrations all look identical to a leaked internal from the outside and are not.
…e contracts (stablyai#23950) Removes ~74 assertions of the form `expect(SOME_CONSTANT).toBe(<literal>)` where the literal is an internal tuning value — a timeout, retry count, debounce interval, cache TTL, circuit-breaker window, Tailwind class string. Those cannot fail for any reason a user would notice: they fail only when someone deliberately changes the number, and then the test is simply updated. They are copies of the declaration. The same pattern is NOT junk when the exact value is observable outside this process, so those were deliberately kept: - terminal byte contracts: `\r`, `\x03` ETX, Kitty escapes, `\x1b[?1;2c`; - wire and capability values: `agent.launch.v2`, protocol 3 / min-compatible 2, daemon per-feature boundary versions (a daemon survives app updates, so those pin what an old field daemon may be trusted with), relay header tokens; - security invariants: the `127.0.0.1` bind default, an empty iframe `sandbox`; - values external processes read: exit code 78 (EX_CONFIG) and exit code 3 (systemd `RestartPreventExitStatus`), `ORCA_AGENT_SESSION_SPAWN_TOKEN`, `npx skills …` commands users paste, on-disk journal schema versions, the `orca_<hash>` filename prefix the fish sweeper matches; - third-party names: expo-router's `unstable_settings` / `ErrorBoundary`, iOS Safari's 16px zoom threshold. Where a case asserted a relation rather than a literal — `A < B`, a sum of parts, a cap compared against a sibling budget — the relation stays and only the literal went. Test-only changes: no production file is touched and no test file is deleted.
…er runs journaled (stablyai#23758) * fix(claude): a subagent resumed after a restart keeps its canonical id A backgrounded Claude subagent resumed by a message after its session's provider restarted parents its frames to its ORIGINAL spawn call, while the announcement the new provider sees names only the resuming call. The spawn call's alias lived only in the old provider's memory, so every row the resumed child wrote fell through to its raw call id: one child shown as two, a named roster entry with no rows and an unnamed section holding them. The alias table now also recalls what an earlier run of the session resolved, re-derived from the agent rows it journaled (canonical id beside the call its frames arrived under), read once per bound journal epoch. Nothing new is persisted. * fix(claude): a restarted provider continues the subagent roster earlier runs journaled The roster's state lived in one provider process while everything it writes is the session's. After a restart a resumed child was re-rostered in a second group row with its attempts restarted, its resumed frames lost the alias they still carry, and the one group row no turn owns was rewritten from empty, erasing the children an earlier run had listed there. A run now reads what earlier runs journaled, once per bound journal epoch: group rows give each child's entry and group, agent rows give the spawn-call aliases and the latest attempt. A group is inherited only when this run's own events reach it, and inheriting writes nothing. An inherited child is reopened by an announcement exactly as an in-process resume reopens it, and by nothing else. The alias recall is one facet of that read. Nothing new is persisted. * fix(claude): an earlier run's subagent takes Claude's restart verdict in one row At restart Claude reports each agent the previous session left running as stopped ("didn't finish before the previous session ended"). The roster ignored it, leaving the entry unverifiable, while the background-task lane, which never saw that agent announced, opened a second, unnamed row for the same agent. The roster now records the verdict on the inherited entry, keeping the time the earlier run lost contact, and still reopens the entry on an announcement. The background-task lane leaves any agent an earlier run rostered to the roster, from the same journal-derived reading. * fix(claude): a restart verdict on a child whose host died invents no stop time A journal reopened after its host died leaves a child unverifiable with no stop time. Stamping Claude's later restart verdict with the current time would show the whole outage as how long the child ran, so the earlier run's stamp, or its absence, is kept. The journal-liveness comment no longer describes the roster as unable to continue from the journal. * fix(claude): a child two journaled rows list stays live in one of them An older build could re-roster a resumed child in a later turn's row, so two group rows list it (67 children in 39 local sessions). Inheriting the second row re-pointed the child to that row's copy, so the copy the resume had reopened was left at working and swept to unverifiable, while the outcome landed on the stale copy. The first row reached keeps the child. * fix(native-chat): no run length for a settled child with no stop time A subagent whose host died without sweeping it has no stop time. After a restart, Claude's own verdict on it ("stopped") is now recorded, but the rule that hides the group's duration only covered `unverifiable`, so a mixed group showed the sibling's duration as the whole group's. The rule now keys on the missing stop time, whatever the settled state. * fix(claude): a twice-listed child resumes in the row the journal reading chose A child an older build listed in two rows was placed in whichever row this run's frames reached first, so a sibling's frame reaching the older row made the resumed child reopen there. Placement now follows the reading's one tie-break, and evicting a row drops only placements that point at it. * fix(native-chat): a resumed subagent's clock never counts the idle gap A reopened Claude child now starts a new run: its startedAt is reset to the reopening frame's time on every reopen path (resume after a restart and a same-process reactivation). The group clock reads the union of the children's latest runs, so an idle gap between runs is never counted; an ordinary overlapping fan-out reads the same as before.
…i#23768) * fix(editor): keep saved untitled notes when their tab closes Saving cleared the draft and dirty flag, so close-time cleanup took a saved note for an untouched placeholder and deleted it. Fixes stablyai#23688 * fix(editor): decide untitled-note cleanup from disk state, not a per-save flag Drop setUntitledFileHasSavedContent and its save-queue hook so deleteUntouchedOnClose keeps its creation-time template meaning. A clean untitled tab now counts as an empty placeholder only while its last-known disk content is empty or not yet loaded; the on-disk size check still gates every actual delete. Notes an agent filled while their tab was showing now stay reopenable with Cmd+Shift+T. Also test both Don't Save dialogs end to end, mirror the shared discard in the floating panel test mock, and remove markFileDirty selectors left without consumers. * test(editor): wait for untitled cleanup before asserting disk state Drain the stat-then-delete chain before the untitled-note tests assert the file survived, so the in-flight Don't Save case fails if the size check is removed. Type the main-window close dialog hook by the controller fields it reads, which lets its test drop a type assertion. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…tablyai#24010) * fix: keep active tab visible by docking to viewport edges Makes the current tab easier to locate in many-tab scenarios. The active tab now sticks to a viewport edge via sticky positioning when it would scroll out of view, with a full-foreground indicator bar for better visibility and arrow animation when a background tab opens off-screen. * fix(tab-bar): reveal offscreen tabs instead of nudge animation When a background tab opens beyond the visible area, automatically scroll to reveal it (unless hovering the tab strip). This replaces the previous arrow-nudge animation with direct visibility. revealTabStripElement now handles keeping the active tab visible alongside the revealed tab when both fit, or docks the active tab when needed. * fix(tab-bar): reveal tabs by identity, not count increase alone Detect opened tabs by comparing tab identities independently of count changes. Newly opened tabs are now revealed even when the total tab count stays the same—e.g., when a tab closes as another opens. * fix(tab-bar): track tabs by identity for reliable reveal on open/close Replace count-based tab detection with identity tracking so the strip correctly reveals tabs when they're added, replaced, or when the active tab closes and switches to a far-back history tab. Removes the tabCount parameter and simplifies overflow navigation by using identity sets. * fix(tab-bar): defer revealing tabs until pointer leaves When a background tab opens while the pointer hovers the tab strip, defer its reveal until the pointer leaves. This prevents the active tab from sliding away mid-interaction. Also support client-hosted rows taking active state while maintaining tab dock positioning.
…ai#24040) * fix(push): size the claim-attempt budget from the drain count With twelve drains, up to eleven peers can hold device heads, so a four-attempt claim budget can run out while claimable rows remain and the drain exits idle for a tick. Move the drain count into one module and derive the attempt budget from it. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * style(push): keep the worker and store in repo formatting Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * style(push): drop the stray semicolon in the concurrency constant Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
… settings (stablyai#23979) * fix(agent-trust): write per-user trust under the home the launched agent reads The Cursor, Copilot, Qoder and Antigravity writers and the local Codex config list resolved ~ with os.homedir() at write time, so any test that reached them wrote into the developer's real ~/.codex, ~/.cursor, ~/.copilot or ~/.gemini. Each writer now takes the home, derived once from the launch env (HOME, or USERPROFILE on Windows, else this host's home) by launchedAgentHome, which the SSH relay already used. * test: give tests that wrote the real agent or Orca home a temp one The structured Codex adoption replay pre-trusted /repos/workspace-1 in the real ~/.codex/config.toml; it now runs with a temp HOME and userData. The Codex session-resume and WSL hook tests created Orca's managed Codex home under the live userData, and the Claude Agent Teams tests wrote their tmux shim into ~/.orca; each now runs against a temp userData or HOME. * test: fail any unit test that writes the real agent or Orca home A vitest setup file wraps the node:fs mutating calls and refuses a target under the account's real ~/.codex, ~/.claude(.json), ~/.orca, ~/.cursor, ~/.copilot, ~/.gemini, ~/.qoder or Orca userData, found through os.userInfo() so a test that swaps HOME cannot hide it. The refusal is recorded and rethrown after the test, since trust writers swallow errors. Reads are untouched. It stands down only while an opted-in real-agent suite's own switch is set. It also unsets what an Orca terminal exports toward the live app (userData, Codex and Claude homes, and the Codex launch preflight CLI, which a shell test would otherwise run), so a local run matches CI. * test: type the guarded fs call from its narrowed original
…tablyai#24043) Completes the `src/main` audit. Sweep over `src/main/runtime` (flat, rpc, orchestration, relay, push) and flat `src/main`. 42 case declarations removed across 24 files, 783 lines gone. No file deleted whole, no production code touched. Highest-yield area by far was `runtime/orchestration` (21 cases from 151 files); the rest of runtime measured under 1%. What went, by pattern: - Tests whose subject is the test itself: a source-scanning boundary test with no production import at all, three of whose cases checked its own regex against strings it declares; and a benchmark whose own simulation contains the short-circuit it asserts, with `expect(wouldHaveBeen).toBe(60000)` comparing the test's own arithmetic. - Identity copiers: seven db cases where every asserted value is the literal input (`type: 'question'` in, `type === 'question'` out). - Permanently skipped tests for behavior that does not exist — two cases carrying `// TODO: inline restore on re-subscribe not yet implemented`. A skipped test for an unimplemented feature can never fail; it is a note in test syntax. - Duplicate invocations of a contract owned at a stronger boundary, including three reset scopes and two dependency-promotion cases owned by dedicated suites. - Registration manifests whose every entry is referenced by other tests. - Table rows and cases varying a field production never reads: a mobile tab-restore case varying `clientCapabilities`, which the mobile branch does not consult, and a Windows worker case in a module with no platform input at all. - Names promising more than the input exercises: a case titled for forged AppImage variables whose body only removes `AppRun`, byte-identical to a row of the `it.each` table twenty lines above. - Negative controls passing for an unrelated reason: a foreign-pane rejection whose fixture also differs in sender handle, so the handle guard can reject it. - Self-comparisons, including `format(m, { authority: 'current' }) === format(m)`. Two deletions were justified by the wrong argument and kept only after checking a better one. "A generated-catalog check gates registration" is false: that script prevents the catalog and dispatcher from drifting apart, so a developer who removes a method regenerates the catalog and the check passes. Like a type annotation over an interface and its implementing class, it verifies internal consistency and cannot see a declaration and its use removed together. Both inventories go on per-entry evidence instead — every name is referenced by other tests. The same rule kept a 37-entry terminal-method inventory in the same wave, because some of its entries are pinned solely by it. An inventory is a ratchet if and only if at least one entry is pinned solely by it; that is evidence per entry, not a verdict by shape. Kept deliberately: everything a gate cites, checked by case title and not only by file path; source-scanning ratchets that pair their negative assertion with a positive one against real source (the surviving boundary case asserts the pattern still matches the writer module, so a silently-broken regex goes red); a destructive-delete PTY waiver guard; `it.fails` markers, which go red if the bug is fixed; and `it.skipIf(platform)` cases, which do run on other hosts. Coverage is partial and stated as such: 1,195 files in scope, roughly 660 read case-by-case, the remainder title-scanned and mechanically triaged. Unread paths are recorded for a later sweep rather than assumed clean. Verified: `check-reliability-gates.mjs` (140 gates), `check:code-quality:changed` (0 new findings). No `pnpm tc` needed since no production file changed. A local run of `src/main` surfaced 42 failures in three files this change does not touch (`browser-manager-tab-identity`, `browser-manager-viewport-ownership`, `session-scanner-codex-workers`); all 42 reproduce on a pristine `origin/main` worktree, so they predate this wave and are environment-dependent locally — CI was green on main at `2d85fdc753e2`.
…pair Orca's duplicates (stablyai#22592) (stablyai#23958) * fix(codex): recognise Codex's quoted project-trust spellings in config.toml (stablyai#22592) Codex's settings screen writes project trust as ["projects"."/p"] and "trust_level" = "trusted". Orca's matchers only knew the bare spelling, so a trust write appended a second [projects."/p"] table (or a second trust_level line) and every codex command then failed with "duplicate key". The config mirror kept both spellings in Orca-managed homes for the same reason. - Project table headers are now read through the existing TOML key-path parser, so bare, quoted, literal-quoted, mixed and spaced spellings are the same table for trust writes and the managed-home mirror/dedupe. - trust_level is found by decoded key, in both the trust writer and the mirror's trust reader, and an existing key is rewritten, never duplicated. - On the next trust write, a table older Orca appended (exactly [projects."<p>"] holding only trust_level = "trusted") that duplicates the user's table, or the bare line it inserted under a quoted "trust_level", is removed; the user's table wins and the atomic writer keeps config.toml.bak. Any other duplicate, or a repair that would still leave one, leaves the file untouched and logs once. * build(cli): list the new Codex trust modules in the CLI project * fix(codex): recognise Codex's quoted hooks.state spellings and repair Orca's copies (stablyai#22592) Codex writes hook trust as ["hooks"."state"."<key>"] (and the parent as ["hooks"."state"]). Orca's hook-trust writer, parent-table check and mirror only knew the bare spelling, so a hook-trust write appended a bare copy and the file failed to parse with "Cannot declare ... twice". - The hooks.state header, parent-table and mirror checks now use the TOML key-path parser, like project tables. - The duplicate repair now also removes Orca's own hooks.state tables (an exact [hooks.state."<k>"] with only enabled + trusted_hash, or an empty [hooks.state]) that repeat a table in another spelling, and runs on hook trust writes too, so a file with both project and hook duplicates is fully repaired. The Orca-shaped copy is removed whichever order the two tables are in, only when exactly one other table (the user's) remains; anything else is left untouched and logged once. * fix(codex): carry plain-Codex plugin and project hook trust into Orca's Codex homes (stablyai#22592) Codex keeps hook trust in $CODEX_HOME/config.toml under hooks.state, keyed by the hook's source. Plugin keys (`id@mkt:path`) and project keys (`<repo>/.codex/...`) are the same in every home, but the mirror dropped every hooks.state table from ~/.codex, so Codex inside Orca asked users to re-trust plugin and project hooks they had already trusted in plain Codex. - classifyHookTrustKey splits keys into home-scoped (the home's own hooks.json/config.toml, re-keyed by install as before) and shared. - The mirror now carries shared hook trust from ~/.codex in every spelling. A key the managed home already holds keeps the managed copy, a key repeated in ~/.codex is carried once, and the parent [hooks.state] table is never copied, so the result never declares a table twice. - mergeSystemCodexConfigIntoRuntime moves to codex-config-mirror-merge.ts to keep codex-config-mirror.ts under the line limit. - Tests cover plugin/project carry in each spelling, user-hook keys staying out, repeated launches, managed-copy precedence, Windows key spellings, parent tables, and user-hook trust re-keying (trusted_hash and enabled) from every ~/.codex spelling. * fix(codex): carry session_end and interrupt hook trust into Orca's Codex homes (stablyai#22592) The shared-trust classifier parsed hook keys with Orca's own trust-key parser, which only knows the ten events Orca installs hooks for. Keys for Codex's session_end and interrupt events did not parse, so their plugin and project trust was treated as home-scoped and left out of the managed home. The classifier now reads the source path from Codex's key shape `{source}:{event}:{group}:{handler}` for any event label. A key without that shape is still never carried. Tests cover both events for plugin and project keys in both spellings, user-layer keys for both events, and five unattributable key shapes.
Group 10 is full; swap the README QR code and copy (all locales) to the new group 11 invite.
…tle (stablyai#23948) * fix(sidebar): keep hook-less agent rows while the agent runs, whatever its title Codex retitles its pane to the project name, so the sidebar's title-derived row (which required the title to name an agent) vanished while Codex kept running (stablyai#23767). Rows now take identity from the canonical pane resolver over the pane's foreground-process read and launch record, then the title; the title only decides idle/working/needs-input. The row still goes away when the PTY exits, the process tracker proves the shell is back, or the title is a shell or default title. * test(dashboard): justify the partial store fixture's type assertion * fix(sidebar): only a live process read keeps a plain-title agent row Review of the previous commit found ghost rows: the tab launch record is a latch nothing clears on WSL, after an SSH exit, or for a launch that never started, and a parked pane's process read went stale because only the mounted tracker re-derives it. - The launch record returns to main's role: a fallback only for titles that show activity, ranked below a title naming another agent (pane reuse), matching the tab icon's order. - A parked pane's command boundary retires its unconfirmable process read, like the mounted ladder's unavailable path; reveal re-reads it. * fix(sidebar): confirm before a parked marker retires an agent; read Git Bash prompt titles as the shell - A parked pane's end-of-command marker can be a nested shell's leak under a still-running full-screen agent, so confirm the foreground first (as the mounted ladder does) and retire the process read only on a shell or no answer. SSH/remote parked panes hold no incarnation to fence a host read with, so they still retire. - Git Bash emits no command marks; its `$MSYSTEM:$PWD` prompt title (MINGW64:/c/repo) is now shell evidence, so a stale Codex read there no longer keeps a ghost row after Codex exits. * fix(sidebar): trust only process-read agents for plain-title rows; per-worktree foreground selector groups by tab A daemon reattach seeds the pane's foreground entry with its launch agent, which can outlive the process while Orca is closed. The entry now records where its agent came from (agentEvidence), and the sidebar/dashboard title-derived rows only keep a plain-title row on an actual process read. Routing and the tab icon are unchanged. selectPaneForegroundAgentsForWorktree grouped every pane key per worktree; it now groups by tab once per map identity and skips worktrees with no tabs. * fix(sidebar): a parked pane's reattach keeps its own process read of the same agent The reattach seed marked a returning parked Codex pane as launch-record evidence, over the process read this session already took, so its row blinked out on reveal and stayed hidden if the user left the tab before the visible read landed. Keep the read when it names the same agent; the seed still drops byte-routing trust. * test(terminal): foreground confirmation publishes process-read evidence * fix(sidebar): a cleared pane title retires the agent's process read Codex clears its title when it exits, and the tab then shows its default title. A pane without shell command marks never re-reads its foreground process, so the retained read kept a "Codex · Idle" row after /quit (permanently for a hand-typed Codex; about 15 s while the marked-pane confirm ladder ran). Treat a blank title like the default title it shows. * fix(sidebar): the pane's process monitor retires an exited agent's process read A hook-less pane keeps its sidebar row from the tracker's foreground-process read, but nothing re-derived that read in a pane without OSC 133 command marks. After Codex exited there, a "Codex · Idle" row stayed: permanently when the shell titles its prompt, or when a killed Codex leaves its last title. The pane's agent-completion process monitor already confirms an agent's exit (no agent and no child processes, held past its settle window). It now reports that exit to the tracker, which retires its own process read and runs the confirmed-shell path the visible-pty read uses. A tracker read that names an agent seeds the monitor, so hidden panes and panes the monitor had not polled yet are watched too. A command read in flight still decides the pane, and launch records or other agents' reads are left alone. * fix(sidebar): a monitor-confirmed exit leaves the next agent in an unmarked pane identifiable The process-exit retire published shellForeground:true and left the one-shot visible sample settled; a pane without command marks has no command start to lift either, so a Codex typed again after quitting was never read and lost its row on retitle. Publish shellForeground:false and reopen the sample. * test(terminal): justify the pane binding cast in the process-exit relaunch test
… agent.launch (stablyai#22954) * fix(mobile): start + menu, quick command and diff-note agents through agent.launch The session screen's + menu, agent quick commands and diff notes' New agent session now ask the host to start the agent with agent.launchReplay, so the host picks chat or terminal from the desktop's default and delivers any prompt. Hosts without the launch capabilities keep today's paths. The phone's pending tab choice is one value (a tab, a terminal by handle, or a launched surface) instead of two refs, and a launched chat is found by its session id in the next snapshot rather than a predicted tab id. A launched surface waits a bounded number of snapshots for its tab. * test(mobile): add the + menu and diff-note launch scenarios to the recording corpus * test(mobile): repin bridged-parity tallies for the four launch goldens; drop test casts The corpus grows from 790 to 794 goldens; all four new ones replay identically. * fix(mobile): show a refused agent launch as a toast beside open tabs The inline create error renders only in an empty session, so a host refusal (for example a disabled agent) from the + menu in a session with tabs showed nothing. Always toast the failure: the caller's own copy when it gave one, otherwise the host's reason. * test(mobile): type the launch reply helper with the shared launch outcome types * fix(mobile): record a launched agent's tab as this device's pick on the host A launch carries no navigation, so the phone selected the new tab only locally while the host kept this device on the tab it had before. Leaving the session and coming back, or a reconnect that reset the screen, reopened that old tab. The "+" terminal path this replaced asked the host to select the tab for the caller. When a launched surface's tab lands in a snapshot, activate it for the caller exactly as a tap does. The resolver now names the landed tab in place of the unused `missed` flag. Route parity re-pinned for the new activation body, identity payload and strings. * fix(mobile): land on a launched agent's tab without a 500 ms wait or a blank pane The host publishes a launched tab before it replies, so the tab list the phone already holds usually has it by the time the reply arrives. The launch paths still waited for a refetch 500 ms later, leaving the phone on the old tab for that long after every launch. Read the tab list at once. On hosts without agent.launch, the chat path also unsubscribed the open terminal and cleared its handle before the chat's tab landed, while the old terminal tab stayed selected: a blank pane until the next tab list. Leave the open tab live until the chat lands, as the launch path does; applying that tab list tears the old terminal down. Route parity re-pinned for the two bodies. * fix(mobile): keep a tab the user picked while a prompted launch was still replying A quick command or review-notes launch now waits for the host to deliver the prompt, which can take up to a minute. The launched tab shows up in the tab row well before that, so a user who tapped another tab meanwhile was pulled back onto the launched one when the reply arrived, and that pick was recorded on the host. The launch now remembers which tab the phone was on when it started and only takes focus if the phone is still there when the reply lands. Any move made in between, by a tap or by the computer navigating this phone, wins. Session route parity re-pinned for the handleCreateTerminal body only. * fix(mobile): name a launched agent's tab before asking, and land on it when it is listed A "+" menu, quick-command or review-notes launch now reserves its tab before it asks the host: a fresh pane key (tab and leaf UUIDs) and, for an agent the host may start as a chat, a session id. Both are minted once per launch and sent unchanged on every replay, since the host's replay fingerprint covers them. The phone arms its pending selection with that reservation before sending, so it lands on the terminal (matched by pane halves) or chat (matched by session id) as soon as the tab is listed. For an agent whose prompt is pasted after start, that is long before the reply, which waits for delivery. Landing also frees the "+" lock; the lock holds the create's id, so an older launch's reply cannot free a newer one's. The reply now only adds its own handle or session id (an older host ignores the reservation), starts the fallback countdown, and reports prompt delivery. A tab the user picks mid-launch replaces the pending selection, so the launch-start tab check is gone. A reservation the host refuses as already taken reads "Couldn't start the agent. Try again." on the first send, and as unconfirmed after a replay. The mobile UUID fallback now yields a v4 UUID, because a pane key's leaf must be one. The host launch path moved to new-tab-agent-host-launch.ts; session route parity re-pinned for that move and the landing's lock release. * test(mobile): expect the launch reservation in the four launch scenarios The four launch scenarios now expect the pane key and session id the phone sends (the scripted ids come first, so the operation id moves from ...001 to ...004). * fix(mobile): don't say an agent may not have started while the user is looking at it When a launch's reply was lost after its tab had already landed, the phone said "Couldn't confirm the agent started", although the listed tab proves it did. Now a listed tab narrows the doubt to the prompt or notes ("The agent started, but couldn't confirm the notes were sent."), the notes stay unsent, and a bare launch says nothing. Only the nested-function parity pin moves, for handleCreateTerminal passing the tab list to the launch. * test(mobile): read the launch's sent reservation through the host's params schema The anti-slop audit rejects Reflect.get; parsing with AgentLaunchReplay also asserts the host accepts the params the phone sent. * test(mobile): check the launch reservation against the host without importing its schema Mobile code may import the params contract only as types. The phone's tests now read the sent reservation by narrowing, a chat reservation is checked through the real host dispatcher, and the older-host drop is pinned host-side. * fix(mobile): don't send the same review notes to a second new agent The "+" lock is now freed when the launched tab lands, but review notes are only cleared when the launch's reply confirms delivery, which for a prompted launch can take up to a minute. In that window "Send review notes to AI" still offered the same notes, and choosing a new agent session started a second agent with them. The notes a new agent session is being started with are now held from the tap until that launch settles: the Send button no longer counts them, the sheet no longer offers them, and a stale tap on the old sheet starts nothing. Notes the host did not deliver become sendable again once the reply arrives. * test(mobile): record the + menu and diff-note launch goldens 4 added (+ as a terminal, + as a chat, notes delivered, notes not delivered). 10 existing create-terminal goldens move only because the recorded state now shows one pending selection instead of two refs; their requests are unchanged.
…ield (stablyai#24052) stablyai#24010's spec read them with Reflect.get, which the low-evidence lint rejects, so every PR's static analysis now fails on main.
* fix(chat): separate text paste from attachments and route by pane Keep composer text independent of image checks and saving, and route pastes caught underneath chat to the originating pane's mounted input. Preserve native event data, selection replacement, undo, and target lifetime checks. Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: lurunzi <lurunzi@gmail.com> * fix(chat): keep focus and quiet text paste after routing it to chat - A paste inserted into the composer now moves focus there, as the old menu-paste insert did; otherwise a paste routed from the hidden terminal left the next keystrokes going to that terminal. - With a remote-server or not-ready workspace, pasted text no longer shows the "Local attachments are not available" refusal because the clipboard also held an image rendition (common for Office copies). The menu path probes for an image only when the text read is empty, so a paired browser does one permission-gated clipboard read for a text paste, not two. - Latest-value refs update in a layout effect instead of during render. * perf(clipboard): answer "is there an image?" from the format list The chat composer asks the main process whether the clipboard holds an image before explaining an image-only paste on a remote-server workspace. That probe decoded the whole image (readImage().isEmpty()) on the main thread just to return a boolean. Read clipboard.availableFormats() instead, and share the MIME check with the paired-web probe. * fix(chat): a chat cover owns focus, so input never reaches the hidden terminal When a Chat UI tab opened over its terminal, the terminal's xterm kept keyboard focus until the composer claimed it a frame later, and forever if the composer never became ready (still starting, a question card, a phone holding input). The previous commits rerouted paste from that hidden terminal to the chat, but typing and Enter still went to the terminal, an image-only or refused paste left focus there, and about twenty terminal.focus() call sites could put it back. Make "a covered terminal cannot hold focus" structural instead: - The chat cover takes focus in the commit that mounts it and marks the covered xterm inert, so every terminal.focus() path is refused by the browser. Split siblings are untouched. When the chat goes away the xterm is un-inerted, and gets focus back only if focus was inside that chat. - Terminal paste listeners skip anything inside a chat cover (previously only inside a mounted chat root). The reroute from terminal to chat is gone; terminal-only paste is back to main's code. - A paste that finds no chat input (before the chat mounts, or an approval card with no text field) gets a visible refusal from the cover. A disabled composer shows the same notice inline instead of dropping the paste. New copy: "Can't paste — this chat isn't accepting input right now." (the old "Worktree not ready" toast was wrong for a chat that is still starting). - The terminal context menu, which names its pane, keeps a small request event to that pane's chat, now without a clipboard payload and using the existing covered-pane check. - Cmd/Ctrl+V or Shift+Insert on a non-input part of the chat focuses the composer (or question answer) first, so the paste lands there. - The composer-scope check used to decide whether a text field inside the chat keeps its own paste matched the whole pane (the file-drop surface carries the same attribute). It now asks the composer whether the target is inside its input. * fix(chat): don't paste a copied file's name next to the file Copying a file in Finder or another file manager puts its name on the clipboard as text/plain beside the file itself. Since text and images are now pasted independently, pasting such a copy into a local or SSH chat inserted the file name into the prompt as well as attaching the image. On the paste-event path, text/plain that is exactly the names of the pasted files (one per line) is the file's label, not prompt text, so it is dropped when an image from that paste is being attached. Rich-text copies (text plus an image rendition) still insert their text, and a copied non-image file, which is not attached, still pastes its name as before. * fix(chat): don't type a Finder file's name on Cmd+V either On macOS, Cmd+V in the chat goes through the app-menu paste, which reads the clipboard text and saves the clipboard image separately. A file copied in Finder also puts its name on the clipboard as text, so the composer typed the name next to the attachment. On main the menu path never read text once an image saved. The main process now reports the paths of the files a file manager copied (macOS filenames plist or file URL, Explorer's FileNameW, a Linux uri-list). Text that only labels those files waits for the image outcome: dropped when an image is attached (or refused on a remote owner), typed when none came. The same label rule now also accepts a path or file URL per line, which is how Linux file managers label copied files on the paste-event path. * fix(chat): pane focus aimed at a chat lands on the chat Since the covered terminal became inert, focusing a pane that shows a chat (keyboard pane navigation, focus-follows-mouse, split activation) was refused and focus stayed on the pane the user left, so typing went to that visible sibling terminal. The one place a pane's focus is requested now puts it on the pane's chat cover, which hands it to the composer when the pane is revealed. Focus already inside the chat is left alone. * test(terminal): give fake panes the container pane focus now reads Pane focus checks the pane's container for a chat cover, and these two fixtures built panes with only a terminal, so four tests threw. * refactor(native-chat): move composer paste handle and chat-root key routing into their own modules Brings NativeChatComposer.tsx and NativeChatResolvedView.tsx back under the 400-line limit after merging main. No behavior change. --------- Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: lurunzi <lurunzi@gmail.com>
…nty (stablyai#23788) * fix(chat): decode Claude pastes and track terminal delivery uncertainty Keep queued prompts pending while the existing agent status reports work, and check fresh history after a later idle fact. Preserve draft text and distinguish write rejection from unconfirmed delivery. Co-authored-by: Harshul Rathod <harshulrathod1640@gmail.com> * fix(native-chat): break the observed-send import cycle and keep renderer tests out of main The observed-send path imported the clear helpers from native-chat-runtime-send, which imports it back. Move the input-clear layer into its own module both use. The Claude paste decoder test imported the renderer pending module from src/main, which the node typecheck project cannot see; the echo-retirement assertions now live in a renderer test. * fix(native-chat): still submit a Claude chat send whose write acknowledgment was lost A remote write whose acknowledgment is lost (timeout, dropped link) is not a refusal, but the observed path stopped there and never sent Enter, leaving a body that did land sitting unsubmitted in Claude's input line until the next send's clear wiped it. Continue to the next write without re-sending the bytes, as the unobserved path always did. * perf(native-chat): keep terminal Chat pending delivery from re-rendering every row The delivery notices were merged into a new Map on every render, which invalidated the transcript row context and re-rendered every memoized row on each stream update. The pending hook also wrote a fresh array on every status ping and prune pass even when nothing changed, and the phone mapped its pending list on every render, rebuilding the chat list data. Memoize the merged notices, skip no-op pending writes, and memoize the phone's rendered pending list. The phone also skips a transcript read when no send is due. * fix(native-chat): never flag a queued Claude send, and flag one an idle Claude never starts Two gaps in when terminal Chat calls a Claude send "Delivery unconfirmed": A prompt sent while Claude is mid-turn is queued, and Claude folds it into the running turn as a queued-command record. The transcript reader drops those records, so once the turn ended the prompt Claude did run read as unconfirmed, inviting a duplicate resend. A send made while the agent is busy is now never checked; it keeps the pending behaviour it had before. A prompt sent to an idle Claude that never starts a turn (Claude exited to the shell, or the paste went nowhere) left the status at the same idle fact forever, so the check never ran and the bubble stayed pending. An idle agent starts a turn on a delivered prompt at once, so a send whose idle status is unchanged after the existing 20 s bound is now checked against a fresh transcript read. * fix(native-chat): add the delivery notice strings to the English catalog The Dismiss action's translate key was missing from en.json, which fails the localization catalog and extraction gates. The desktop "Message not sent" and "Delivery unconfirmed" notices were hard-coded English; route them through translate with the same wording. * fix(mobile): sync the held-send refs after commit instead of during render Moving the acknowledgment-loss hold into its own hook made its render-time ref writes new lines, which the React Doctor changed-lines gate blocks. Held sends report after commit, so syncing those refs in a layout effect keeps them current where they are read. * fix(native-chat): report only definite terminal Chat send outcomes The delivery rule inferred "Delivery unconfirmed" from "the turn ended and the transcript has no matching row". Claude records a prompt sent mid-turn only as a queued-command attachment, which the transcript reader drops, so that rule flagged prompts Claude had answered. It also never fired for an idle Claude that lost the write, because no newer turn arrives. Keep only facts the transport reports: - a refused write reads "Message not sent", keeps its text, and can be dismissed; - a lost write acknowledgment holds the echo for 20 s, the phone's existing rule, then reads "Delivery unconfirmed" unless its row has landed. An ordinary send, including one Claude queues mid-turn, stays pending as before. Remove the agent-status subscription, the status-epoch origin, the fresh 500-row transcript read, the confirmed state and the no-status clock. The phone already implements this rule, so its changes revert to main; only a test for old-host paste envelopes remains. * fix(i18n): translate the terminal Chat delivery notices Add the Dismiss, "Message not sent" and "Delivery unconfirmed" strings to the es, fr, ja, ko and zh catalogs, reusing each catalog's existing Dismiss wording. * fix(native-chat): let a resend replace its failed terminal Chat echo A "Message not sent" or "Delivery unconfirmed" echo kept its transcript occurrence, so resending the same text numbered the resend as the second copy: the one landed row retired the failed echo and pinned the resend below the reply forever. Appending a send now drops a failed echo with the same content first. * fix(native-chat): unwrap a Claude paste that quotes pasted_content tags The envelope parser refused any body containing a pasted_content tag, so a pasted prompt that itself quotes one (a transcript excerpt, or code that handles these tags) kept its wrapper and its echo stayed pinned below the reply. Claude's per-paste id exists to disambiguate exactly that; only a same-id tag inside the body is now ambiguous. Wrappers without an id keep the strict rule. * test(native-chat): pin which terminal Chat sends observe write outcomes Only a Claude chat send (text or images) reports a refused or unacknowledged write to its pending echo; other agents and slash commands keep the unobserved write path exactly as before. * fix(native-chat): keep failed terminal Chat sends through Stop Stop cleared every optimistic echo, including a "Message not sent" or "Delivery unconfirmed" bubble whose send had already settled. Stop cannot affect that send, and the bubble is the only place its text stays copyable, so it now survives until the user dismisses or resends it. Also moves the observed-send import below the file header comment. --------- Co-authored-by: Harshul Rathod <harshulrathod1640@gmail.com>
…lyai#23806) * fix(rate-limits): stop driving a hidden Codex TUI to read usage When the headless Codex usage call failed, Orca opened a hidden interactive Codex, typed /status and pressed Enter without reading the screen, then killed it after 15 s. If Codex showed its "Update available" prompt, that Enter picked "Update now", Codex started its installer, and the 15 s kill interrupted it, leaving the global install broken. Drop the hidden-terminal fallback. When the headless call fails with a non-sign-in error, read the same usage from the HTTP endpoint Orca already calls on WSL and for the 5-hour window, so a usage check can no longer answer any Codex startup screen. Fixes stablyai#17415 * test(rate-limits): pin the RPC error when the Codex HTTP fallback fails Also drop comments that still described the removed hidden-terminal fallback. * test(rate-limits): use real Response objects in Codex fetcher tests The changed-code gate rejects the new type assertions this PR added.
… so phone swipes never type escape text (stablyai#23943) * fix(mobile): preserve host mouse modes for terminal scrolling * revert(terminal): drop the out-of-band mouse-modes channel The earlier commit sent the host's mouse tracking and encoding beside each snapshot and stream frame, and made the phone trust that over what its replayed bytes say. Where the snapshot text already carries the encoding this is redundant, and where the text is wrong (a desktop pane snapshot never writes ?1006h) the host's own mirror is seeded from that same text, so the side channel asserts the wrong encoding too. Remove the shared type, the host publication, the wire fields and the phone's host-modes authority. The phone keeps its guard: while mouse tracking is on but no replayed byte proved the encoding, a wheel scrolls locally instead of sending a guessed legacy report. * fix(terminal): carry the mouse encoding in desktop pane snapshots Swiping a phone terminal running Codex typed `[M`-style mouse bytes into Codex's prompt instead of scrolling (stablyai#23818). Codex turns on mouse tracking with the SGR encoding (?1006h). A desktop pane snapshot is made by xterm's serialize addon, which writes the tracking modes but never the encoding, so the phone replayed "tracking on, legacy encoding" and sent legacy `ESC[M` reports that Codex does not parse. The host's own terminal model is seeded from the same snapshot, so it lost the encoding too. The pane now mirrors its xterm's mouse encoding from the parser (1006, 1016 and a full reset, with xterm's own set/reset rules) and its snapshot ends with the matching DECSET, after the alternate-screen switch that readers keep. Programs on the default encoding get nothing appended. The phone keeps a guard for hosts without this fix: while a wheel- reporting tracking mode is on but no replayed byte proved the encoding, a wheel or swipe scrolls locally and a tap or drag sends no mouse report (the tap still focuses the keyboard). Click-only tracking (?9h) keeps its arrow-key scrolling on the alternate screen, and an explicit ?1006l still sends legacy reports. * fix(terminal): state the default mouse encoding so legacy mouse apps keep phone input Snapshots now say ?1006l while a program tracks the mouse with the default encoding (daemon rehydrate and desktop pane serializer), and the phone treats a tracking enable seen in live output as proof of its encoding. Legacy-encoding programs such as vim with mouse=a keep phone scrolling, taps and drags; replay-only tracking with no stated encoding still sends nothing. An unproven drag falls back to local selection, and an empty pane snapshot stays empty. * chore(terminal): type the mouse-encoding tracker inputs for the low-evidence audit
* fix(claude): run Windows hooks without shell operators Keep neutral replies inside the managed entry and payload scripts, repair missing files from managed registrations, and stop using Git Bash discovery to guess Claude's hook shell. Co-authored-by: latte271 <junghyeyun27@gmail.com> Co-authored-by: Bing.Z <zzb@gxsmjx.com> * fix(claude): keep the Windows hook refresh async and its scripts after uninstall - List windows-hook-files.ts in the CLI project so typecheck passes. - Refresh the entry/payload pair only from a surviving entry, with an async existence check, so startup refresh stays off the main thread on Windows. - Keep both scripts on uninstall like every other agent; a Claude session still holding old settings keeps answering instead of erroring per event. - A payload that exists but cannot start falls through to the neutral reply, and the missing-payload branch exits early for background jobs. - Update the EDR posture reference for the operator-free command. * test(claude): run the Windows hook host legs for real The live Windows host legs never ran: runProcessSync cannot take a string stdin (it forces encoding 'buffer'), so every leg threw before starting a host. Use async runProcess, pass PATHEXT (without it Windows PowerShell 5.1 prints nothing and exits 0 for a .cmd path), and name the host in each assertion. Drop the POSIX pwsh leg: its drive-mapping shim proved nothing about Windows, and the Windows legs cover both PowerShell hosts. --------- Co-authored-by: latte271 <junghyeyun27@gmail.com> Co-authored-by: Bing.Z <zzb@gxsmjx.com>
…y name (stablyai#24065) Audit sweep over `src/renderer` (3,206 test files in scope). 58 case declarations removed across 37 files, 3 test files deleted, 1,033 lines gone. Nine dead production symbols removed with them. The dominant pattern this wave was a case whose input cannot reach the behavior its title claims: - `getRemoteBrowserFrameStyle` takes `_metadata` unused and returns a constant object. Four cases fed it 958x609, a uniform high-DPI 1998x1218, an uneven high-DPI frame, and malformed metadata, all asserting the same constant. The function cannot branch on any of them; the runtime rejects server-sized frames, so the renderer ignores bitmap dimensions by design. Kept the one case whose name admits that. - `shouldIgnoreTerminalMenuPointerDownOutside` takes `{openedAtMs, nowMs}` and has no button or modifier parameter. Two cases named for secondary-button and macOS control-click passed byte-identical timestamps. Their titles came from the docblock's prose, which describes behavior the CALLER delivers. - A retry case titled "then Retry re-arms" proved that half by deleting a key from its own ref object and asserting it was undefined. The `stablyai#6648` budget assertion stays; the title now matches what the case proves. Also removed: - Cases that cannot fail: three asserting `toBeInstanceOf(Map)` and `size === 0` immediately after a `beforeEach` that clears those maps. - Assertion-free coverage probes, one of which writes a JSON report to tmpdir and asserts nothing while its own comment says the sibling case is the gate. - Copied inventories and export lists, each checked per entry: a 35-name renderer git-client export list (every name has 7-40 production references) and a 16-path caller census that never verified the callers pass the argument it exists to track. - Provider-local replays of one factory (`createUsageProviderSlice`), keeping a single representative; and a case re-running a shared window-shortcut policy the shared suite owns. - Duplicate invocations, including a `buildNativeChatSendBytes` block whose function is `buildNativeChatPasteBytes(text) + '\r'`, with three titles verbatim from the paste-bytes cases. - A slice case whose only assertion reads back `antigravity: null`, a literal in `createEmptyRateLimitState()`, under a title claiming a "stable pending key". Nine production symbols went, each verified to have exactly its own declaration plus one test reference and zero production callers: `buildNativeChatSendBytes` (its docblock said "kept for callers/tests" and had none), `isCommandMarkerId`, `buildNativeChatRenderItems`, `collectToolResults`, `scrapeNativeChatSession`, three now-orphaned types, and an eagerly-evaluated `WORKTREE_CARD_PROPERTY_OPTIONS`. One deletion was reverted. An i18n JSX-spacing guard reads `.tsx` source text and would break under a behavior-preserving rewrite, both true. But all three components it guards have zero referencing test files, so nothing else catches `translate('…','Detected from')}` followed by `<code>` rendering as "Detected fromorca.yaml". The junk patterns describe a test's shape; the ratchet rules describe consequence, and consequence wins. Kept deliberately: a randomized parity test measured at 483 of 517 comparisons being `identity === identity`, because 32 seeds and 2 named cases are genuine differentials and the fix is a rewrite, not a deletion; a title-tracker parity test whose two sides are separately-authored implementations that a sibling case proves can diverge; a Monaco upstream-drift detector that reads the installed package, which is the shipped contract; and nine bound guards, all confirmed to have live production callers. Coverage is partial and stated as such: roughly 810 of 3,206 files read case-by-case, the rest triaged by mechanical scan. Every auditor disclosed its own unread set and those paths are tracked rather than assumed clean. Verified: `pnpm test` over all nine renderer chunks — 2,834 files, 25,540 cases, 0 failures (a first run showed 5 failures that did not reproduce and were concurrent load); `pnpm tc` after clearing `.tsbuildinfo`; `check-reliability-gates.mjs` 140 gates; `check:code-quality:changed` 0 new findings. All three deleted files confirmed absent from the gate manifest, `cloud/package.json` and `mobile/tests-typecheck-baseline.txt`.
…tablyai#24064) Hermes fires `on_session_start` when a session is opened, switched or reset; its payload carries no turn. Orca mapped it to `working`, so a freshly launched Hermes held a fresh first-party `working` row for the whole 30-minute staleness window: `terminal wait --for tui-idle` never settled against an idle composer, and the sidebar span a phantom spinner. Map it to `done` with `sessionBoundary`, matching what Claude and the compatible-lifecycle providers already do for SessionStart, and let the tui-idle evidence lane settle on a fresh boundary row. A boundary row claims a new session owns the pane and awaits its first input, which cannot arrive mid-turn, so it carries none of the stablyai#6011 risk that scoped that lane to DSH; a turn-end `done` still does not settle. Fixes stablyai#13653. Co-authored-by: Brian Grablin <5216789+bgrablin@users.noreply.github.com>
…each (stablyai#24077) Completes the first pass over every test area in the repository. Sweep over `mobile/src`, `config/scripts`, `cloud/`, and `tests/` (1,494 files in scope, with the 24 files under `mobile/src/test-support/rpc-recording/` deliberately excluded). 31 case declarations removed across 17 files, 2 test files deleted, 356 lines gone. What went, by pattern: - Cross-boundary replays of a shared helper. A whole mobile file re-ran `extractPendingAsk`/`parseAskFromStatus`/`formatAskAnswer`, all owned by `src/shared/native-chat-ask.test.ts`, `native-chat-ask-fifo.test.ts` and the renderer's interactive-prompt suite — one case title was verbatim identical to the owner's, and the owners' inputs are supersets. The mobile file imported the shared module directly and exercised no mobile transport, lifecycle or rendering. - A case whose input cannot reach the behavior its title names: "arms it on Android while the drawer is open", where `use-back-claim.ts` has zero Platform/OS references, so flipping the mocked OS changes only shadow styles. - Identity copiers, including one asserting `prSidebarRenderBranch(state) === state.kind` against a production body that is `return state.kind`. The function stays; it has three live callers. - A test of the runtime rather than the product: a case asserting Node's own `EventEmitter` crash contract on a bare emitter, with zero production code in the path. The guard it documents is exercised behaviourally by the case after it. - Duplicate invocations, one of them provable rather than eyeballed: with `MODULE_SCOPE_ENV_WRITER_PIN = 0`, `files.size <= 0` is strictly implied by the sibling's `expect(offenders).toEqual([])`, since a non-empty `offenders` forces `files.size >= 1`. The pin's own doc says it may only ever be decreased from 0, so it could never become a meaningful bound either. Its policy guidance survives as a comment; the file's real ratchet and its regex self-test both stay. - Expected values produced by the test's own arithmetic, and a p95 case strictly implied by a sibling that already pins exact p95 and exact max over a wider range. One production line goes: the `export` keyword on `assignmentCleanupSteps` in `cloud/apps/relay/src/assignment-cleanup-steps.ts`. The function itself stays and is still called internally; only the test-only export was orphaned. Kept deliberately: everything a gate cites, checked by case title and not only by file path; a gate-cited case that does not deliver its claim (reported instead — see below); a cross-version wire cell whose ledger is never invoked, left under the raised bar for wire coverage; and every limit, bound, quota and provenance guard. Nothing under `mobile/src/test-support/rpc-recording/` or `mobile/rpc-foundation/goldens/` was touched — those bytes feed a `recorderSha256` digest pinning 398 golden recordings. Verified: `mobile` vitest over the modified mobile files (8 files, 50 cases); `mobile/scripts/check-tests-typecheck-ratchet.mjs` OK (898 files in program, 125 grandfathered, none @ts-nocheck); relay suite 799 passed; `check-reliability-gates.mjs` 140 gates; both deleted files confirmed absent from the gate manifest, `cloud/package.json` and `mobile/tests-typecheck-baseline.txt`. Seven local failures were investigated and none is caused by this change: five `mobile-web-app-*-render` tests drive `playwright-core` chromium/webkit and need browsers this machine lacks, `release-checkout.unit.test.ts` needs cross-version git refs, and `e2e-worker-env-isolation.unit.test.ts` fails identically with its HEAD content restored — it recurses `tests/e2e` with symlink-following `statSync` and no depth guard.
…ed (stablyai#24048) * feat(secrets): warn in Settings when a credential is stored unencrypted When no OS keyring is usable, the MiniMax stores write the credential as a plaintext envelope and say so with a console.warn nobody reads. The users this affects are exactly the ones who never see a main-process log, so in practice they were told nothing (stablyai#21827). Report it where the credential is managed instead. Each store gains a protection reader, the status IPC carries it, and Settings renders a warning next to the credential it applies to. Keyed on the stored bytes, not isEncryptionAvailable(): a credential saved before a keyring existed stays plaintext until it is saved again, so reporting current capability would call it protected while the file says otherwise. The readers parse the envelope kind without decrypting, so opening Settings cannot provoke a keychain prompt. The console.warn stays. It carries no secret material, and it is still the only signal on a headless host with no Settings window. * feat(secrets): extend the unsealed-credential warning to every affected store The speech key, Linear tokens, Jira tokens and the Bitbucket credential have the same plaintext fallback the MiniMax stores do, and the same console-only warning nobody reads. Add a shared `readCredentialFileProtection` for the four stores that write bare ciphertext with no envelope, classifying with the same printable-UTF-8 test `readStoredCredentialToken` already uses — so the reporter cannot drift into disagreeing with the reader about the same bytes. Linear and Jira report across every stored workspace/site rather than the active one: sealing is a host-wide property, so a second workspace stored while the keyring was missing is exposed even when the active one is sealed. Both fields are optional, so an older remote host that omits them reads as unknown rather than as sealed. Bitbucket reports null for env-supplied auth, where Orca stores nothing and has no claim to make. Also fixes the credential-connection test double, whose identity-function `encryptString` wrote a readable token — faithful enough for a round-trip assertion, but it made the suite assert that a sealed credential was exposed. * chore(i18n): extract the unsealed-credential notice strings CI's localization-extraction gate requires every translate() key to exist in the primary catalog. Inserted in place rather than re-sorting the file, which is not fully sorted and would have produced a 17k-line diff. * test(web): pin the null protection fields on the desktop-only MiniMax bridge The web bridge reports no protection because it stores nothing; the shape assertions had to move with it.
…ct (stablyai#24035) * fix(secrets): stop telling Linux users to install a keyring they already run On a desktop Chromium does not recognise (Hyprland, sway, river, niri) the selected backend is basic_text and sealing is unavailable, so the at-rest protection report told the user to install and unlock gnome-keyring — which is usually already running and serving org.freedesktop.secrets. Chromium simply never looked, because it picks the backend from XDG_CURRENT_DESKTOP. Split the Linux unavailable path on the selected backend: an unrecognised desktop now says so, names XDG_CURRENT_DESKTOP, and points at --password-store. A backend that did resolve but cannot seal keeps the install-and-unlock text, which is correct there. The backend read stays after isEncryptionAvailable(), the call that performs the D-Bus probe, so nothing new blocks (STA-5765 timing unchanged). * fix(secrets): seal credentials on Linux desktops Chromium cannot detect Chromium picks its os_crypt backend from the desktop-environment env vars and recognises none of the tiling compositors (Hyprland, sway, river, niri). On those it selects basic_text, whose key Electron only exposes after an explicit setUsePlainTextEncryption() this app never calls — so isEncryptionAvailable() is false and every credential store takes its plaintext fallback, while gnome-keyring sits on the session bus unasked (stablyai#21827). Name gnome-libsecret ourselves, but only where it provably cannot hurt: - Never for a desktop Chromium does resolve. Overriding a working selection is the one change that could strand already-sealed credentials, and a KDE session identified only by KDE_FULL_SESSION is the case that matters. - Only when the secret service's default collection is present AND unlocked, measured out of process with a killable 1.5s deadline. A locked collection with no unlock prompter is what made isEncryptionAvailable() block for 76s to first window (STA-5765); selecting libsecret there would trade silent plaintext for a frozen app. The probe reads the Locked property, which cannot itself trigger a prompt. Anything unexpected — no session bus, no gdbus, no name owner, timeout — leaves Chromium's own choice alone, so the worst case is today's behaviour. The desktop-presence env vars come from a review finding by @Raajik on stablyai#21831, confirmed there against base/nix/xdg_util.cc.
…ey name (stablyai#24101) Sweeps the triage-only backlog: 2,269 files that earlier waves saw and skipped for size, reconstructed from the unread lists five waves of auditors disclosed. 35 case declarations removed across 15 files, 2 test files deleted, 487 lines gone. No production file touched. These are large integration suites, so the junk here is individual cases buried among real coverage rather than whole bad files. The dominant defect was again a case whose input cannot reach the behavior its title names: - `resume-sleeping-agent-session-remote-compat.test.ts` (deleted) — two cases titled for "transport-level host authority on a capable host" and "host authority is not known". `resume-sleeping-agent-session.ts` has no host-authority or capability concept at all, and its only read of `origin` is `if (!record.origin && record.state === 'done')`, unreachable for both rows. Both executed one identical path. The surviving contract is owned by `resume-sleeping-agent-session-execution-host-scope.test.ts`, which drives a real host catalog. - `project-group-header-drag.test.ts` (deleted) — four cases setting `data-project-group-header-id`, which the predicate never reads. Its subject, `isProjectGroupHeaderActionTarget`, is byte-identical to `isRepoHeaderActionTarget` apart from the function name and imports the same `REPO_HEADER_ACTION_SELECTOR`, so all four cases were a strict subset of `project-header-drag.test.ts` using identical `data-repo-header-*` fixtures. - `remote-worktree-history-cleanup.test.ts` — "repeats idempotent cleanup through the PTY owner" against a six-line best-effort forward with zero dedupe state. The case called it twice and asserted the mock recorded two calls, which is arithmetic over the test's own loop; nothing about idempotence was established. Also removed: - Runtime assertions of type-level facts, where production already makes the check at a stronger boundary: `const adapterSatisfiesPort: AdapterIsPort = true` followed by `expect(...).toBe(true)` — unconditionally true, while `createExpoGenerationFileSystem(): GenerationFileSystem` is explicitly annotated and passed into `createGenerationStore` at a typed call site. And a case named "does not typecheck" whose runtime assertion is a length check on its own literal, declaring its own local annotation so it could never notice the production annotation weakening. - Private predicate tests duplicated at a real boundary: four `repo-slug-cache` cases delivered by `repo-slug-index.test.ts`, which drives the same resolution through the hook, the real store and the preload bridge, while the cache-level versions hand-seed the internal map and break on a cache-key format change. - Duplicate invocations owned at the shared boundary, including commit and push recovery cases owned by `src/shared/source-control-recovery-agent-command.test.ts`. Kept deliberately, verified rather than assumed: the production duplication behind the deleted drag test was left alone, because `REPO_HEADER_ACTION_SELECTOR` ends in generic `button, a, input, textarea, select`, so genuine action targets inside a group header still match — it is an unspecialised copy-paste, not a live bug, and collapsing two functions is a refactor. Reported instead. Auditors' probes produced 20, 11 and 13 candidate hits for the signature-versus-title shape across their chunks; every one was inspected and every one was genuine coverage. No deletion in this wave rests on a probe alone. Coverage is partial and stated as such: of 2,269 files, roughly 100 were read case-by-case and the remainder reviewed at title-plus-import level. Each auditor listed its own unread set. The largest remaining surfaces are `src/main/agent-hooks` (95), `src/main/claude` (100), `src/renderer/src/lib/pane-manager` (62) and the 20 largest sidebar suites. Verified: 2,583 desktop test files / 25,636 cases pass, plus one pre-existing `it.fails` marker; the two modified mobile files pass (57 cases); `check-reliability-gates.mjs` 140 gates; `check:code-quality:changed` 0 new findings. Both deleted files confirmed absent from the gate manifest, `cloud/package.json` and `mobile/tests-typecheck-baseline.txt`.
…blyai#24094) * docs(security): add the antivirus clearance path for future releases Every AV false positive here has been handled one vendor and one shipped version at a time. Document the programs that clear future releases instead -- signer and product enrollment rather than per-build sample submission -- and add a script that reports an RC's current detection state by hash, so a verdict is found before users meet it in an issue report. Hash lookup only by default; --upload transmits the artifact and stays manual. * fix(windows): replace the managed CLI launcher with a native one resources\bin\orca.exe was a csc-compiled MSIL assembly: a small, freshly compiled .NET image in a user-writable directory that mutates environment variables and proxies a child process. That is the shape .NET dropper heuristics are trained on, and every verdict against it named the family -- MSILHeracles from two vendors, Wacatac!ml from a third. Signing the file does not change its shape, so signing never cleared it. Rebuild it in Rust. Same resolution, same environment contract, same argv passthrough that keeps newline-bearing orchestration bodies intact (stablyai#8374), and the child still inherits our environment block rather than an explicit map, so a block carrying both PATH and Path survives (stablyai#12046). The PE now carries publisher, version, icon and an asInvoker manifest from build.rs. Refs stablyai#23383 * ci(windows): install the Rust toolchain before building the CLI launcher The hosted runners happen to ship cargo, but a real Windows dev box does not -- verified on our own Windows QA host, where cargo and rustc were both absent. Relying on the image means a future image change fails deep inside electron-builder's native hook instead of at an obvious step.
Upstream range 31012ae..d2dbe2c, 94 commits. Each conflict was resolved to upstream's file plus exactly the fork's delta: - ai-vault/runtime-session-scanner.test.ts: upstream stablyai#23992 removed the runtime-host restamp case; the fork's resume chain (02c0d49) added the applied-scan-depth case beside it. Took the removal, kept the fork's case. - daemon/daemon-endpoint-publish.test.ts: upstream stablyai#23953 removed "replaces an incumbent that has stopped listening"; the fork's stale-bundle drain (1a813da) added two handed-over-incumbent cases beside it. Took the removal, kept the fork's cases. - startup/desktop-startup-ordering.test.ts: upstream stablyai#23976 cut the file to the three cases its reliability gates reference; the fork's mac self-updater (5690d15) added one health-marker case inside the deleted block. Took the cut, kept the fork's case. Semantic conflict, no markers: - shared/release-sources.test.ts: upstream stablyai#23829 removed the *_PRERELEASE_IDENTIFIER exports the fork's reserved-identifier test (31241cd) imported, which broke the typecheck. The test now derives the dev channels from RELEASE_CHANNELS and hasDedicatedReleaseRepo, both still exported; upstream's file is unchanged. - daemon/daemon-pty-router.ts: upstream stablyai#23925 (+7) and the fork's stale-bundle drain (1a813da, +33) merged cleanly to 302 counted lines, over the 300-line max-lines limit. Joined the drain method's spawnAttachOnly object literal onto one line, as upstream writes the same call above it; no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review complete. No issues found — approved ✅. The PR diff is empty (a merge of upstream main into swaplabs/main with no fork-visible changes), so there is nothing to review. Reviewed commit: f963cc4 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Daily sync run (auto-sync): leaving this PR open for the operator. 54 checks pass and 1 fails. Codex found nothing on Failing:
The one allowed re-run was refused: Wiki release notes to add under Unreleased once this mergesReplace **30 September 2026 · Upstream sync, [fork PR #29](https://github.com/SwapLabsInc/orca/pull/29), on `swaplabs/main` and not yet in a numbered build**
- **Upstream through `d2dbe2c385`** (94 commits since `31012aeb09`; 28 of them only remove tests
upstream judged weak):
- **Native chat:** a message sent mid-turn waits as an editable card above the composer, on the
desktop and the phone. A failed turn ends on its error, and an unfinished `/clear` or a refused
Codex rewind no longer locks the chat.
- **Claude and Codex:** Stop acts at once and is no longer lost between turns. Codex terminals
run without Codex's shared background server unless a new setting opts back in, and Orca
pre-trusts the folder wherever it starts an agent.
- **Terminals:** phone swipes no longer type escape text into Codex, and dropped images whose
file names need escaping now attach.
- **Secrets:** Settings warns when a credential is stored unencrypted, and Linux desktops that
Chromium cannot detect now encrypt credentials.
- **Windows:** a native launcher replaces the managed CLI launcher.
- **Fork changes:** none in behaviour. Three test files conflicted where upstream deleted tests next to ones the
fork added, and one fork test used constants upstream removed. Each keeps upstream's change plus
the fork's tests; no fork behaviour changed.
- **Also on `swaplabs/main` and not listed above until now:** the 29 September upstream sync to
`31012aeb09` ([fork PR #22](https://github.com/SwapLabsInc/orca/pull/22)), and
[fork PR #25](https://github.com/SwapLabsInc/orca/pull/25): on a host that sets
`ORCA_DAEMON_DRAIN_STALE_BUNDLE=1`, the first launch after an update starts a fresh terminal
daemon for new terminals, while the old one keeps its terminals until the last one closes.
Fork PRs [#23](https://github.com/SwapLabsInc/orca/pull/23),
[#24](https://github.com/SwapLabsInc/orca/pull/24) and
[#26](https://github.com/SwapLabsInc/orca/pull/26) change only fork CI and review tooling.
[Changes in this sync](https://github.com/SwapLabsInc/orca/compare/64dea21a911b...MERGEHEAD) |
ELI5
Upstream Orca moved forward 94 commits, and the fork's copy of upstream (
main) has caught up with it. This PR merges those commits into the fork's integration branch,swaplabs/main. Git could not do that on its own, so the merge is made here, on a sync branch, and every place where upstream's changes and the fork's changes met is listed below.What Changed
Upstream range
31012aeb09..d2dbe2c385, 94 commits (compare), merged in f963cc4.Conflicts. Most of this upstream range deletes tests upstream considers weak, and three of those deletions sat right next to tests the fork added. Each file now holds upstream's version plus exactly the fork's added tests:
src/main/ai-vault/runtime-session-scanner.test.tssrc/main/daemon/daemon-endpoint-publish.test.tssrc/main/startup/desktop-startup-ordering.test.tsTwo more breakages merged without a conflict marker and are fixed in the same merge commit:
src/shared/release-sources.test.ts: upstream test: stop testing private internals through exports no caller needs stablyai/orca#23829 removed theHOURLY/DAILY/ADHOC_PRERELEASE_IDENTIFIERexports that the fork's reserved-identifier test (31241cd) imported, which broke the typecheck. The test now derives the dev channels fromRELEASE_CHANNELSandhasDedicatedReleaseRepo, which upstream still exports. Upstream's file is unchanged.src/main/daemon/daemon-pty-router.ts: upstream fix(terminal): the terminal's owner answers colour queries for the terminal's whole life stablyai/orca#23925 (+7 lines) and the fork's stale-bundle drain (+33) merged cleanly to 302 counted lines, over the 300-linemax-lineslimit. The drain method'sspawnAttachOnly({ ...opts, attachOnly: true })object literal is now on one line, the way upstream writes the same call a few lines above it. Formatting only, no behaviour change.Why
This is the daily run of the "Orca upstream sync" automation. #28, opened from
main, conflicted, andmainmirrors upstream and takes no commits, so the merge lives on this branch.Linked Issue
N/A: daily upstream sync.
Visual Proof
N/A: merges upstream changes and changes no fork UI.
Testing
Run locally on minidev-alex1 before pushing:
pnpm tc: passes.updater.server-update-help-url.test.tsneedsORCA_SERVER_UPDATE_HELP_URLunset, and this host sets it.pnpm testover the directories the resolution touched (src/main/ai-vault,src/main/daemon,src/main/startup,src/main/runtime, and the other files both sides changed): 21 failures in 4 files, all environment-only and identical to upstream. 15 are fixturegit commitcalls this host's signing wrapper refuses, 2 need/bin/zsh, which isn't installed, and 4 are bash 5.3 prompt-hook differences.pnpm run check:code-quality:changedwith the sync-PR scope CI uses (ORCA_SYNC_PR_DIFF_BASE=d2dbe2c385): 0 new findings across 250 files.oxlint, the React Doctor lint config andoxfmt --checkover every staged file: pass.Notes
orca-fork-ops statusforswaplabs/main(64dea21a91) against upstreamd2dbe2c385:LOCAL:prefix and no upstream PR the report could matchThe missing upstream release tag
v1.4.217was copied to the fork before this PR opened.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.