Repository navigation
Sync upstream 1.4.214 - #7
Merged
Merged
Conversation
* Add direct CSV table editing and persistent column widths * Preserve pending CSV edits across concurrent saves and panes * Keep CSV input stable during autosave and table commands * Cancel delayed menu paste after CSV pane focus changes * Prove delayed CSV menu paste starts before focus changes
Expand numeric-leading Linear identifier coverage in the current parser and workspace source flow while rejecting numeric-only team keys in bare inputs and URLs. Main already implements the production behavior through stablyai#23423. Repair two stale localization test assumptions found by full CI. All changes are regression tests. Validation: 369 focused tests across 46 suites, local typechecking/lint/formatting, and full final-head CI passed. Co-authored-by: Ken Fukuyama <kenfdev@gmail.com>
* fix: move worktree script checks out of creation dialog * fix: retain creation host across background preparation
…ipt scrolls (stablyai#22871) * fix(mobile): stop Android from selecting words while the chat transcript scrolls On Android every paragraph, heading, quote, code block and table cell in the native chat transcript was a selectable TextView. Android starts a word selection, with the magnifier, on a double tap or a long press, and two flicks in the same spot while scrolling a FlatList register as a double tap, so scrolling the chat kept selecting words. iOS is unaffected: its UITextView path arbitrates scroll against selection itself. Android now renders transcript text without inline selection: one gate in MarkdownText covers every selectable span, and the user bubble follows it. A long press on a message opens a sheet with "Copy message" and "Select text", the latter a screen whose only content is one selectable Text, so a selection can only start where the user asked for it. The message row owns that sheet and mounts it only while open. iOS and web keep their inline selection and get no long-press handler. Verified on a Pixel 10 Pro Fold (Android 17): an adb double tap on the transcript selects nothing, the same double tap inside "Select text" selects a word, tool rows inside the bubble still expand on tap, and the long press opens the sheet. * fix(mobile): scope the Android selection gate to the transcript and route span long presses Review follow-ups on stablyai#22871: - The gate now applies only where `rangeSelectable` is passed (the chat transcript). Task comments and file previews keep their selectable text on Android as before. - On the Android transcript, spans that take taps (links, file paths) also take the row's long press, so a link under the finger no longer swallows the copy/select sheet. - Copied text keeps its whitespace; only whitespace-only blocks are dropped. - The Android markdown test compares `String(node.type)` instead of a type assertion, which the changed-code quality gate rejects. * fix(mobile): route long presses on Markdown images to the row on Android An image block is a Pressable of its own, so on the Android transcript it now carries the row's onLongPress like tappable spans do; a long press on an image opens the copy/select sheet instead of being swallowed. Test extended with an image block. * test(mobile): pin the Android long press from a chat row to its actions sheet The existing row suite runs as iOS, where the bubble has no long press. This one runs as Android: the bubble's long press mounts the actions sheet with the message, the markdown receives the same handler, closing unmounts the sheet, and the user bubble carries no inline selection. * fix(mobile): preserve Android message selection while replies stream --------- Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: kazooooo-ma <zooooomer-com@gmail.com>
…ablyai#25485) fix(codex): restart account switches without blanking or repinning the pane Integrate the atomic pane replacement from stablyai#24350 and adapt the explicit conversation resume from stablyai#14877. Keep terminal protocol replies flowing while account notices block user input. Ordinary restores retain provenance. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: itisvincent <vincentcgamer@gmail.com> Co-authored-by: rayim <rayim@fxy.global> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gin (stablyai#25428) * fix(opencode): submit initial prompts for reviewed 2.0.12 Route the reported release through the existing native startup intent. Retain conservative behavior for other unreviewed versions. * Wait for OpenCode startup fixture file reads in the hydration test --------- Co-authored-by: nwparker <nwparker@users.noreply.github.com>
… or anyone (stablyai#24376) * feat(jira): pick the assignee when creating an issue — Automatic, me, or anyone The new-issue dialog gains an Assignee picker: Automatic (Jira applies the project's default assignee), one-click "Assign to me" resolved from the target site's stored identity, or any user via search. The selection resets with the custom field values on project/type switches, since account ids are site-scoped. Creates now also send userFieldKeys, so the host shapes user-typed values into the {accountId}/{name} refs Jira requires — previously the dialog never passed them and user create fields were sent as bare strings. Keys are only named when a value is present, keeping older remote hosts without the user-fields capability working for assignee-less creates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(jira): address assignee review — project-scoped assignable search, per-site picker remount, keep assignee across type changes - Search the create dialog's assignee picker against /user/assignable/search?project=… (new listAssignableUsersForProject, IPC split into jira-user-search.ts for the max-lines budget); remote environment targets fall back to the existing site-wide search since older hosts have no such RPC - Remount the picker per target project so cached results from the previous project or site cannot be selected after a switch - Reset the picked assignee on project selection (account ids are site-scoped) instead of on every create-fields reload, so issue-type changes keep it - Hide the assignee field and omit it from the payload when the target create screen does not accept an assignee — Jira rejects fields absent from the screen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(jira): scope create assignee to the effective project and provider * fix(jira): isolate assignee search candidates across provider changes * fix(jira): search assignable users with Server-compatible project keys * fix(jira): refresh assignee identity with provider connection status * fix: distinguish hook confirmation from browser confirm --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai>
…i#25425) Name the local hook confirmation function to avoid misclassifying internal selectors as dialog text.
…5480) Keep IME confirmation out of text-field submission and command selection. Reuse shared gesture ownership, preserve marked-release redispatch, guard overlay Escape capture, and retain Markdown save shortcuts. Verified with independent adversarial reviews, 891 tests across 71 files, six hidden Electron tests, project typecheck, full lint, the changed-code quality gate, and all final-head CI checks. Fixes stablyai#25035. Co-authored-by: kambarakun <kambarakun@gmail.com>
…ith a boundary check (stablyai#25329) * refactor(terminal): move the topology revision and leaf-lookup helpers into terminal-topology advanceTerminalTopologyRevision, findTerminalTabIdForLeaf and hasHostAuthoritativeTerminalMembership move verbatim from the renderer-save membership rebase into persistence/terminal-topology, the home of the commit boundary. Importers are repointed; no behavior change. * test(terminal): test-only guard for topology writes outside the commit boundary The three session sinks now publish through one commitWorkspaceSessionPartition helper, which hands the prior and published partition to the topology write guard. Production never arms the guard, so each sink pays one global lookup. The unit suite arms it in report mode: a sink write that changes class-(a) topology (membership, root, bindings, titles, incarnations, sleeping records, remote session ids, tombstones, default-applied, revision) outside a commit scope is attributed to its writer's file by stack, and fails the test unless the writer is on the unrouted-writers allowlist that later routing PRs shrink. Renderer saves and test seeding are exempt. Deep-freeze is available but stays off suite-wide until the in-place writers return new sessions. * refactor(terminal): add the topology commit module with bindLeaf, closeLeaf and closeTab terminal-topology-commit.ts is the boundary for class-(a) terminal topology. bindLeaf forwards to persistPtyBinding, whose write now runs in a commit scope; the spawn commits and the relay reattach bind through it. closeLeaf and closeTab wrap the existing close mutation in a commit scope and one persistence.terminal-topology span (kind, outcome, refusal reason; no ids), and the runtime close goes through them. Session output is unchanged: tests compare it byte for byte with the old writers for local, ssh: and folder workspaces. * chore(terminal): drop an unused lint suppression from the topology write guard * fix(terminal): attribute topology guard writers relative to the repo root The guard read a frame's file through its last /src/ segment, so a test under tests/ (folder-upgrade-identity-persistence.unit.test.ts) had no source frame and failed as an unknown writer. Frames are now taken relative to the repo root the setup passes in, tests/ counts as test seeding, Windows backslashes are normalized before the node_modules skip, and nested src/ paths keep their full path. The two runtime funnel files skip only their funnel function, so another writer in them still shows. The R9 allowlist key names the file whose frame actually writes. The class-(a) diff and the attribution move into their own files; the stack limit is restored in a finally. * refactor(terminal): drop bindLeaf until binding reaches a sink; add the boundary ratchet bindLeaf and the commit scope inside persistPtyBinding changed nothing: the binding write never reaches a session sink, and a scope inside the Store method would have admitted every direct caller once it did. Both return in B1-4; the spawn commits and the relay reattach call persistPtyBinding directly again. The runtime close now calls one closeLeafOrTab entry, so its callbacks keep their contextual types. A census test is the primary enforcement: only persistence/terminal-topology and the callers it lists may call persistPtyBinding, the session setters or the three sinks, and every unrouted-writer allowlist entry must name an existing file. * fix(test): resolve the topology guard's repo root without the global URL Under happy-dom the global URL is not Node's, so fileURLToPath(new URL(...)) threw in the setup file and failed every happy-dom test file. * refactor(terminal): drop the runtime topology write guard; the boundary ratchet enforces The AST boundary ratchet is the enforcement for B1. The stack-attributed runtime guard, its class-(a) diff, the unrouted-writer allowlist, the vitest setup and the freeze option are removed; it saw two writers in the whole unit suite and its real value starts only once binding reaches a sink (B1-4). Also: one closeLeafOrTab wraps the close in the span (no per-kind copies or narrowed types), the span has one finish like persistence.pty-binding, the sink helper is publishWorkspaceSessionPartition (it publishes; the commit boundary is the module), the ratchet drops the private publishSession row and checks that every listed caller file exists, and the close comparison keeps its two meaningful cases with span cases chosen by name. * refactor(terminal): trim the B1-1 commit module and ratchet to what they enforce - Point the acknowledged-tab-retirement audit fixture at the moved advanceTerminalTopologyRevision; its old import no longer resolved. - Drop publishWorkspaceSessionPartition: it was the removed guard's interception point, so the three session sinks return to origin/main. - One traced(kind, mutate) wrapper in the commit file replaces the span factory; closeLeafOrTab is one call. - The ratchet walks src/main with the shared scanSourceTree, drops the loading-store-internal rows and the redundant file-exists test; exact-set equality already fails on a missing file. - The commit test is a pure unit test of the span outcomes: no Store harness, electron mock or self-comparing close. * refactor(terminal): census the runtime session controller's write and drop stage ids from comments The controller's setter was named set, which the boundary census could not list without matching every Map.set, so a new OrcaRuntime mixin could write sessions through it unseen. Rename it setForWorktree and census it. Comments now describe state instead of citing plan stage ids. * refactor(terminal): census writer references and trace refusals by callback - traced() takes refusalOf instead of assuming an Error refusal, and only mutate() sits in the try, so a span outcome of threw means the write threw. - The boundary ratchet counts references, not just direct calls: non-null calls, bracket keys, aliases, destructures, .call/.bind and parenthesized callees all count; declared names and type positions do not. - Census terminalSurfaceCloseMutation (boundary-only) and the partition sinks setLocalWorkspaceSession / setHostWorkspaceSession. * test(terminal): count writer uses in extends clauses and instantiations, skip type-only imports and local declarations The census skipped ExpressionWithTypeArguments as a type, which also holds `extends f(x)` and `x<T>` value expressions. Type-only import/export specifiers and declared names (variables, parameters, accessors, enum members) no longer count as uses. The audit fixture is listed in the table instead of a separate exemption. * test(terminal): count quoted and assignment-pattern destructures of layout writers * test(terminal): count every mention of a layout writer except its definition Telling definitions from uses per syntax kind kept missing nested and for-of destructures. Exempt only the writer's own function or class-member definition; any other mention (including object-literal keys) counts, so the census errs toward a loud false alarm rather than a silent miss. Quoted names count only in member-name position. * test(terminal): count every string literal naming a layout writer Member-name positions missed wrapped keys like store[('name')] and store['name' as const]. Counting every string literal outside types is shorter and errs toward a loud false alarm. * test(terminal): exempt only class members and functions as writer definitions Object-literal methods and accessors were exempt while equivalent arrow properties counted; all object-literal keys now count alike. * test(terminal): parse files with unicode escapes in the writer census A name spelled with a \u escape never appears verbatim, so the text prefilter skipped it. * test(terminal): parse any file with an escape in the writer census \x, identity and line-continuation escapes also decode to a writer name without it appearing verbatim.
…blyai#24864) * fix(native-chat): a Stop binds only the turn it actually stopped A Stop pressed before any turn showed used to claim, at end-write time, whatever turn the stopped send later opened, even when the Stop stopped nothing. A turn that then died on its own read as "Interrupted" (your cancellation) instead of "Failed". Now a person's Stop that named no turn binds, in memory only, every turn that ends while the Stop settles, and afterwards only the turn its interrupt took. The settle ends a still-running stopped turn once. A relaunch finds nothing in memory, so an unsettled turnless Stop binds no turn. A Codex Stop whose answered turn does not open within its wait, or whose send's answer was lost, now answers refused, so the host ends the child and the turn can never run. * fix(native-chat): keep the person's queue pause and close binding after a Stop settles A host stop or eviction with no turn running now defers to a person's Stop while its queue pause still holds with nothing sent since, read from rows, so a held card is not handed off on reopen after a Stop that did nothing or whose kill failed. A person's close that named no turn opens a settle around its child's end, so a turn that end cuts reads as theirs. A press opens its settle only when the latest Stop event is its own or the one in force it repeats: a late Stop, a card's interrupt or a lost event row reopens no earlier Stop. A Codex Stop that cannot reach a turn still able to open says the Stop is unconfirmed rather than that no turn ran, and a second Stop still reaches a turn an earlier wait left unopened. Also drops the unused openedBy plumbing and the unreachable "a written cancellation stays one" rule, and pins a relaunch after a named Stop. * fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle A Codex Stop that waited for a turn Codex answered a send into now answers "may still open" whenever that turn neither opened nor ended and its send is still owed, however the wait ended (it ran out, or the thread went idle). Before, a first press after an idle thread said no turn was running and kept Codex, while an identical second press ended it. Adds a test that a person's close the conversation outlives (as /clear does) closes its settle, so a later turn that ends on its own reads as a failure. * test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop With one ordered journal writer the Stop's event is in the fold when its write returns, so the press reads whether it owns the latest Stop from the fold instead of awaiting the write. Pins the case the read must refuse. * test(native-chat): name the settle, not a stream drain, in the Stop's own-end test * test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout Codex answers an interrupt it took before it sends turn/completed (interrupted): on TurnAborted the app-server answers pending interrupts, then ends the turn, on one channel. The test fake did the reverse. It now answers first and ends the turn on a later read, and the tests that read the turn's end right after a Stop wait for it. New end-to-end test through the shipped host, journal and Codex adapter: with the real order, every end row of the stopped turn reads interrupted by the Stop (named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the settle window turns the in-flight case red: the Stop's own end row then has no verdict, which reads as failed until Codex's end lands. * fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once A Stop that named no turn, made after Codex answered a send but before the turn opened, used to wait up to 5 s for the turn to open before interrupting, and ended the Codex process when it didn't. The stated reason, that Codex refuses an interrupt until it opens the turn, holds only part of the time: with no turn active, Codex takes an interrupt once its thread runs (turn_interrupt_inner), and refuses it with -32600 "no active turn to interrupt" before that or once the turn has ended. The Stop now sends the interrupt at once. Only on that refusal, while the turn has neither opened nor ended, does it wait for the turn to open (bounded at 5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One that never opens, or that an earlier wait already gave up on, fails the Stop, and the host ends the child as before. Sends still wait for the turn to open before steering into it. The test fake models Codex taking an interrupt once the thread runs (run()). * fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did A Stop whose interrupt Codex refused as finding no active turn skipped the wait when an earlier wait, a Stop's or a send's, had already given up on that turn. Every press now waits its own bound and retries once if the turn starts, so a turn that opens during a later press is still stopped. Both presses still reach the same verdict when it never starts. * fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send A Stop on a Codex turn that was answered but never opened ends the Codex process. That end settled the send as in doubt (unknown, recovered), and the client's outbox holds every later send behind a send in doubt until the person presses Retry, which re-sends the very message they stopped. The chat looked stuck. Codex records a prompt only once its turn has started, so a send whose turn never opened never ran. When the Stop's refusal says so (turnMayOpen), the child end now settles the unanswered sends as withdrawn, the verdict Codex's own interrupted-turn end already gives an unechoed send. The flag rides on the owed wind-down, so a retry after a failed child end withdraws them too. Claude's child end still leaves its unanswered send in doubt. * fix(native-chat): derive the withdrawal of a Codex send whose turn never opened Replaces the flag the Stop carried to the child's end, and its copy on the owed wind-down, with a reading of the journal at the settlement that lands. A Codex child's unanswered send is withdrawn when a person's Stop is in force since it was sent and no turn row ran, or was written, after it; any other end (a turn that opened, a host's close, a crash, Claude) still leaves it in doubt. A retried wind-down reads the same rows, so it withdraws the same sends. * fix(native-chat): keep a send in doubt when a turn was open for it The derived withdrawal read a turn as open for a send only if it still ran or was written after the send. A send steered into a running Codex turn whose interrupt failed met neither once the adapter's end settled that turn ahead of the host's settle, so it read withdrawn, though Codex drains a steer into the running turn and may hold it. A turn that ended after the send was handed over was open for it too: such a send stays in doubt, as before. Pins that case, and that a send made after the Stop, to a child that then dies before its turn opens, stays in doubt. * fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing Whether a turn was open for a send was read from end times: a turn that ended after the send's handover counted. A send made while a Stop ended the turn is handed over once that turn reads ended, yet Codex's own end for it can arrive later, so such a send whose own turn never opened read in doubt again, and the chat's queue held behind it. The handover already records where the send went: its message joins the turn running then (a steer) or belongs to no turn (it starts its own). Only a send that started its own turn, with none opened since, is withdrawn; one that joined a running turn, or has no recorded place, stays in doubt. The Codex test fake takes an answered interrupt as Codex does, dropping the turn before its end arrives. * fix(native-chat): leave no Stop row when the Stop took back a send that never ran A Stop on a Codex send whose turn never opened ends the child, and the child's end takes the send back into the composer. The Stop still wrote "Cancellation requested." at the conversation level, so with the send gone it sat under the previous finished turn and read as if that turn had been stopped. A Stop that found no turn running and whose child end took back every send it found now writes no row; a Stop of a running turn, or one that leaves a send in doubt, still does. * fix(native-chat): count a send whose answer was lost when a Stop takes it back The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart. * test(native-chat): pin which sends a Stop's child end can take back A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted. * test(native-chat): read the outbox reconcile from where main moved it
…corded on its tab, not its workspace id (stablyai#25081) * test(native-chat): a mirrored paired chat carries its host, so a workspace-id collision no longer hides its owner * fix(native-chat): a chat's finished-turn alerts come from the host stamped on its tab, not its workspace id * fix(native-chat): the attention bridge reads a chat's owner from the same rule as the chat pane
…t load says why once (stablyai#24576) * fix(native-chat): an older Orca keeps a chat with newer content read-only, and an unreadable annotation costs only itself A body or lifecycle mutation of a kind this build does not know, inside a row of a known kind, now latches the chat read-only with every row kept, as a newer row kind or version already does. It used to read as damage, and the open deleted the journal from that row on. Each optional field of a body schema now declares what an unparseable value means: - droppable (an annotation): removed from the row in memory; the chat stays writable; - must-understand (a prompt's questions, a plan approval's plan, a turn's lifecycle, a goal change): the row is unreadable and the chat latches read-only. Only a required field that fails is damage, repaired as before. A test holds every optional field reachable from a body schema to a policy; the known body kinds are read off the schema. Prompt options and questions no longer reject a key this build does not know, which deleted the journal from that row on. The context-usage drop is now one case of the droppable rule. * feat(native-chat): a chat an older Orca keeps read-only says so before a send is refused A chat a newer Orca wrote opens read-only, and until now nothing said so: the person found out when a send failed with "Chats were saved by a newer Orca". The host now names the reason on every whole history page it serves (`page.readOnly: 'written-by-newer-orca'`: hydration, history and catch-up resets), the shared client reducer keeps it from the latest whole page, and the desktop status strip and the phone's composer area say "This chat was saved by a newer Orca, so it's read-only here. Update Orca to continue this chat." Every read-only latch the host has is a newer Orca's (its database, a row version, a row kind, a body kind or must-understand fact), so the reason is derived from the journal's latch, not stored. The field is optional: older clients ignore it (cross-version test against the newest release's reducer), and older hosts never send it, so a new client against one says nothing, as today. A reason this client does not know shows nothing rather than words that may be wrong. The window-merge helpers move out of the client reducer into their own module. * fix(native-chat): only a newer build's value goes read-only; damage repairs as before An older Orca keeps a chat read-only only on evidence a newer build wrote it: a value outside a closed set this build knows (a body kind, a nested discriminant or literal, a mutation kind), read off zod's issues. That evidence wins over damage beside it. Anything else that fails (a wrong type, a missing field, a bad optional value) is damage, repaired as before, so damage no longer freezes a chat behind an "update Orca" it cannot fix. Drops the per-field droppable/must-understand policy. A lifecycle mutation's kind is decided before its item id, so a newer kind without one is kept. The context-usage drop is unchanged from main. * fix(native-chat): a read-only chat locks its composer and says why there Removes the separate read-only line (desktop status strip, phone notice component). The host's `readOnly` on whole pages now feeds the existing composer lock instead: desktop disables the composer with "Saved by a newer Orca. Update Orca to continue this chat." as its placeholder, and the phone adds a 'read-only' input-lock reason with the same words. A read-only chat shows no prompt card it would refuse; the draft stays in the composer. The phone's send-failure line is back exactly as on main. * fix(native-chat): a read-only chat pages back through its history A read-only journal answered every history request with a reset, so a chat this PR now keeps read-only more often could show only its newest page. Backward reads now serve the snapshot the open folded, as the first page already does; a forward read of rows still resets. * test(native-chat): an unknown key in a question entry is read and kept * test(mobile): the read-only overlay test typechecks * chore(native-chat): satisfy the changed-code gate in admission and the read-only overlay test * fix(native-chat): a new value inside a known block or goal arm reads as a newer build's The open fallback arm of the block and goal unions now aborts, so zod reports the known arm's own failure instead of only the fallback's; a future closed set there reads unreadable, damage there still repairs. The schema headers name the context-usage exception and say a new open-string value must be safe for every older build (rewind keeps only known states; pages carry no row version). * fix(native-chat): a read-only chat reconnects for a whole page, so an updated host unlocks it The client keeps `readOnly` until a whole page replaces it, but reconnected at its cursor and got only batches, so a desktop attached to a host updated in place stayed locked with "Update Orca". While it holds the latch it now subscribes without a cursor; the snapshot re-derives the state on any host version. The phone already subscribes without one. * fix(native-chat): every desktop write control follows the one read-only fact The structured session's transport state derives one fact from the page's `readOnly`: the words for why the host refuses writes. Every write control reads it. While it holds, the chat has no turn and no work, as the host projects it, so the working row and Stop go away; a pending prompt stays shown above the composer with its card disabled; queued cards' Send now, Delete and Edit, the queue's Resume, the goal banner's actions, the model and option pickers, dictation and background-task Stop are disabled or withheld. The composer placeholder stays the one place the reason is said. `lockReason` now locks the composer by itself. * fix(mobile): every phone write control follows the one read-only fact The phone session derives one fact from the page's `readOnly`, the words for why the host refuses writes, and every write control reads it: no turn or work while it holds, so no Stop; the pending prompt stays shown with its card disabled; queued cards and Resume are inert; the model and option pickers stay shut; the composer field is not editable, so its placeholder keeps saying why, and the lock applies at once instead of after the transport lock's settle. * test(native-chat): the newest release hydrates a read-only chat and scrolls back through it * test(native-chat): a read-only chat reads idle even with a send its provider never answered * test(native-chat): composer lock test without type assertions * fix(native-chat): round-3 read-only fixes - An old /model or option request no longer reopens its picker when a read-only chat unlocks: the menu is keyed by the request alone, and a lock only keeps it from mounting open. - A disabled question card still steps through its questions; only answering is locked. - A failed send's notice in a read-only chat says only that the message was not sent, with no Retry: the composer already says why, and no retry can land. Its wiring moves into useStructuredAgentSessionDeliveryNotices. - The host's background-task stop flags stand; the client no longer overrides them. - The view reads the read-only fact through one local flag. - The schema header says what an older build's rewind does with each open string. * test(native-chat): count picker mounts without an effect * fix(native-chat): read-only keeps other failures' words and shuts open option menus - In a read-only chat, only a send the newer-Orca refusal stopped is shortened to "Your message was not sent."; any other saved failure keeps its own words, and none offers a Retry. - A model or option menu open when the lock lands has its items disabled, so it cannot send a change the host would refuse. * test(native-chat): lock-lands picker test uses a typed surface * fix(native-chat): a locked composer draws its reason The editor's placeholder used the default that draws nothing while the editor is not editable, so a read-only chat's composer was empty and its reason lived only in the aria-label. The placeholder now draws while disabled, and its words are empty unless the composer is locked with a reason, so every other disabled composer (a pending prompt, no terminal) looks as before. * fix(native-chat): sync the locked-placeholder flag in an effect, not during render * fix(native-chat): a read-only chat is not offered for resume or counted as failed to resume After a relaunch, a chat whose journal this build keeps read-only (a newer Orca's) was listed in "Resume interrupted chats?", failed with "Orca couldn't resume this chat. Open it to continue manually.", and left a status-bar "N chats failed to resume" that never cleared, since nothing can continue a chat this build cannot write. The resume set now skips such a chat after the checks that end an offer (fork, moved on), so the offer is kept, not spent, and an updated Orca offers it again; the failure ledger keeps an already-filed failure for it but does not show it. Both read the chat's open journal, which listing and acting open first. * fix(native-chat): a refused resume continuation keeps the refusal's reason A send refused while continuing a chat after a restart was filed with its code alone, so a newer Orca's refusal lost its `journalWrittenByNewerOrca` reason. The continuation now carries the whole refusal, as a refusal from the agent's start already did, so the filed failure keeps it. * fix(native-chat): a newer Orca's chat keeps its resume offer however it is met - A newer Orca's whole database counts as read-only for resume, so a chat whose journal cannot be opened at all (a table the newer schema changed) is not offered, run or counted either. - The checks made right before sending no longer skip read-only chats: turning one away there was read as the user having moved on and deleted the offer. Its send is refused instead. - Settling a resume files nothing for a newer Orca's refusal; the existing rollback reopens the offer for an updated Orca. - The continuation records the refusal as a reference, without the wire prose. * chore(native-chat): one import of the session wire types in the continuation * docs(native-chat): resume and read-only comments match the current rule * test(native-chat): a closed set of a journal row cannot change without the row version The test walks the body schema and the row and mutation kind tables, collects every closed set (a row kind, a mutation kind, each enum, literal or discriminant), and compares them with a snapshot recorded beside the row version. A new value makes older builds go read-only, so it fails until the reader ships first or `v` is bumped and the snapshot updated. Tags a catch-all arm accepts as any string (block types, goal states) are listed apart: older builds read a new one as-is. * feat(native-chat): a body or plan subject of a newer kind is kept and the chat stays writable An item body kind and an approval subject kind this build does not know now read through the same catch-all blocks and goal states use (`openDiscriminatedUnion`), instead of making the chat read-only. The item is kept, drawn by nobody, and ignored by everything that reads items (turns, prompts, status); a rewind carries it as it was, kept by its place like every other row. An approval whose subject this build cannot draw shows its `detail`, which Orca's writers fill with the subject's text. A sent message of a kind this build does not know stays a newer build's. * fix(native-chat): rewind narrows a retained body by its kind before normalizing it * fix(native-chat): one chat that cannot open no longer stops the startup restore of the rest * fix(native-chat): a damaged chat fails to load and nothing is deleted A row this build cannot parse, a gap, or an epoch without its first row used to be repaired on open: every row from the damage on was deleted and a rebuild from the provider transcript was attempted. The open now fails through one refusal (failLoadOnJournalDamage, journalCorrupt), every row stays, and a damaged per-chat file is kept whole and never copied. A newer build's row still wins and keeps the chat read-only. The body classifier for closed-set values, the repair marker and disclosure, and journal recovery from the transcript go. * chore(native-chat): no reset or adapter left on the open's fixtures * feat(native-chat): an approval of a newer Orca's subject kind can only be cancelled Desktop and phone show its detail and the line "This request needs a newer version of Orca.", disable every answer, and keep the card's cancel, which ends the turn. The chat stays writable. * test(native-chat): the closed-set guard says a missed version bump fails older builds' load * test(native-chat): the closed-set guard names what a missed version bump does * fix(native-chat): a row the reader rejects is never written, and a chat that cannot load keeps its tab Every journal insert reads its serialized row back with the reader inside the write's transaction and refuses one it rejects, so Orca's own writer can no longer leave a chat that fails to load. A restored chat whose open fails keeps its tab and says why when opened. An epoch named with no rows is founded afresh again, deleting nothing. The at-rest test now expects a damaged chat to be refused; the replay gate keeps its 2026-09-11 evidence as it was run. * fix(native-chat): a newer Orca's approval subject is carried as it was, and its card cancels with or without a turn The approval subject's type is open, so code that reads a plan narrows first; the host's prompt bounding carries an unknown subject instead of rebuilding it as a plan, which threw in every start's stale settlement. A Codex rewind keeps a newer Orca's item in its place. A whitespace-only tag is damage. The card's cancel reaches the host without a running turn on hosts that take that. A provider row the reader rejects ends its turn as interrupted and the next send works. A guard fails if an approval subject kind is added before clients can be gated (STA-9262). * fix(native-chat): a card's cancel sends nothing without a turn again, and rewind keeps a newer row after any held row The host's cancel request has to name a turn on every version, so the turnless prompt cancel is reverted on desktop and phone. The rewind merge moves its anchor for every row the provider still holds, so a newer Orca's row stays after one whose kind this build does not know. * fix(native-chat): a card this build cannot answer leaves the composer open, and a send starts a turn An approval of a subject kind this build cannot draw, raised by a newer host's background agent while no turn runs, left nothing to press: its answers are disabled, its cancel needs a turn, and the desktop composer gave the card its slot. A send the host queues waits behind any pending prompt, so even the phone's open composer only queued. While every pending prompt is one this build cannot answer, the desktop composer stays open, and desktop and phone send without asking to queue: the send starts a turn, and the card's cancel then settles it. A chat a dead run left is unaffected: opening it already cancels those prompts. * feat(native-chat): a chat a newer Orca saved fails to load and says to update, with nothing to send into Opening a chat a newer Orca saved (a row kind, batch-change kind or sent-message kind this build does not know, a newer row version, or a newer history store) used to open it read-only: the history shown, the composer locked with a reason, every control greyed. Now it fails the load, like a damaged chat, with its own words: "This chat was saved by a newer Orca. Update Orca to open it." Every row is kept. The read is final, so the pane stops retrying; reopening the tab reads again. A chat whose load failed for good (damaged, or a newer Orca's) offers no composer under the error, on desktop and on the phone, so the failure is said once and no send can be refused a second time. A restart offer for a newer Orca's chat is spent without a word: no failure filed, nothing counted. Removed with the read-only mode: the journal's read-only latch and every check of it, the history page's readOnly field (never released), the cursorless reconnect, read-only paging, the composer lock and placeholder, the greyed controls, and the resume exclusion. * test(native-chat): a newer Orca's records open no chat, and a status fake carries its submissions * test(native-chat): the status fake's cast says what the feed reads * fix(native-chat): a chat's read that failed for good takes the whole pane, over a loaded transcript too * test(native-chat): the final-read-failure test names its reasons * fix(native-chat): a newer Orca's chat keeps its restart offer, and its refused load is logged once * refactor(native-chat): the newer-Orca chats a restart listing skips live beside the candidate reader * fix(native-chat): a refused chat load keeps where it failed, so the log names it and a new reason logs again * chore(native-chat): the reducer's window merge back where main has it, and comments say a newer Orca's chat fails to load * test(agent-hooks): the rename test's journal fakes carry submissions, as a journal snapshot does * test(agent-hooks): the rename test's journal fakes say what the status feed reads * fix(native-chat): Dismiss all ends the restart offers this Orca lists, and keeps a newer Orca's hidden ones * test(native-chat): a resend against a newer Orca's store answers unknown, and the phone says nothing beside the read's words This build never opens a chat a newer Orca's database holds, so a resent send id there cannot be answered from its journal: it answers "outcome unknown", never a refusal and never a made-up record, with no second delivery and no write. On the phone, a send error left beside a read that failed for good is not shown; the pane keeps only the read's words.
…stablyai#25421) * fix(codex): stop prompting a Codex restart when only its home changed A Codex terminal that outlived the move to ~/.codex was blocked behind "This Codex session is using an outdated configuration" until the user restarted it, which starts a fresh codex and ends the conversation. That terminal keeps working on Orca's old Codex home, which is refreshed from ~/.codex, so the change did not need the user's answer; the prompt also never appeared for Codex in Git Bash terminals, whose process tree Orca cannot see through. The restart prompt now covers only an account switch, which the user caused. The pane record keeps its home route, which the retained-home refresh and the shared-server check still read; only the prompt and the custom-home downgrade that existed to keep it from guessing are gone. * refactor(codex): drop what the home-route prompt left behind - Record a pane's own CODEX_HOME override as-is. The filter that kept only overrides Orca could re-derive existed for the removed route comparison; its one remaining reader, the shared-server check, returns null for the shared-home route those panes record either way. - Treat custom-home like shared-home in resolveCodexPaneHome: the removed downgrade only wrote it without an override, so it never named a home. - Inline the restart notice key; its route/account prefix was its only job. - Delete the two pane-local override tests, including a POSIX-only one that still expected custom-home and would have failed on Linux and macOS CI. - Cover the account recheck branches the deleted route-recheck file was the last to exercise: main reporting a pane current, and main not answering. * refactor(codex): retire custom-home and the last re-check helpers - Read an older build's custom-home record as shared-home, which it always was, and drop custom-home from the route type and every check. - Delete shellStartupCodexHomeOverrideMatches and its comparison helper; nothing re-checks a recorded override any more. - Build the pane launch record in one return: the route is always set, and only a resumed launch differs, in how it picks the account. - Drop the restart dialog's notice key; its focus effect now depends on the pane and both account labels directly. - Give the account recheck test a typed window stub, so CI's type-assertion gate passes, and remove timing entries for deleted test files. * fix(codex): drop the removed dialog strings main added to es.json * refactor(codex): stop recording a pane's custom CODEX_HOME The pane record kept a pane's CODEX_HOME override so the removed route prompt could re-check it later. Its one other reader, the shared-server check, only looked at it for a real-home pane, and a custom CODEX_HOME always routes a pane to Orca's mirror, so it was never used. Drop both record fields, their validators, equality checks and spawn plumbing. getCustomCodexHomeOverrideForLaunch folds into the existing hasCustomCodexHomeOverrideForLaunch, and real-home resolves to ~/.codex. Records from older builds still parse; the parser keeps only known keys. The fish test's decoy no longer sets CODEX_HOME, so the boolean check still fails if the launch env XDG_CONFIG_HOME is ignored. * test(codex): drop setup the yes/no CODEX_HOME checks no longer need --------- Co-authored-by: Orca Worker <orca-worker@localhost>
…yai#25413) * test: add legacy worker recovery persistence snapshot budget tests Rollback requires the final stored state after processing all workers, not intermediate snapshots after each candidate. This bounds the snapshot count and preserves concurrent edits during the durable write. * fix test * clean up code
…tablyai#25579) The paired web client has no Jira preload, so window.api.jira.status() resolves undefined from the fallback proxy. Since stablyai#24376 the first status check stored that undefined and the sidebar crashed reading `.connected`. Parse the status reply at the runtime-client boundary so every reader gets a well-formed JiraConnectionStatus.
…hem after readiness (step 1 of 7) (stablyai#24257) * feat(agent-launch): host-side prompt delivery for agent.launch The host's agent.launch typed any launch prompt into the shell as part of the launch command. A long or multi-line prompt then ran line by line in the shell, and an agent that never showed readiness or crashed at startup had nothing guarding where its text went. agent.launch now carries a prompt on the typed line only when the line stays one line, control-free and at most 512 bytes; otherwise the agent starts clean and the host pastes the prompt once the agent's own ready signal fires (bracketed paste plus its composer marker or a quiet render, read only after the shell's last hand-off, never while the pane's own shell is proven in front), with main's draft-paste bytes and an Enter 50 ms later. Orchestration worker starts wait on tui-idle as before. A replay-safe launch admits and claims its ledger row in one write, Qwen Code gets a second Enter, the desktop and phone share one launch-refusal classifier, and hosts advertise agent.launch.prompt-carry.v1. Split out of stablyai#23748, which moves the desktop source-control buttons onto this path. * fix(agent-launch): keep a short-lined multi-line prompt on a local zsh launch line, as main did stablyai#24257 moved every multi-line or over-512-byte prompt off the typed launch line and pasted it after readiness. The phone's AI buttons and review notes, whose multi-line prompts main typed whole into zsh, then reached Claude 0.5-3 s later and their RPC reply waited for the paste. The host now names the shell a local macOS or Linux line is typed into, the way the spawn picks it, and a multi-line prompt rides a zsh line when every line is at most 512 bytes and the whole line at most 8 KB. A real-zsh test types such a line through Orca's own ready barrier and startup write, including when a slow user config makes the write land early. Elsewhere the measured unsafe cases keep the paste: bash 3.2 runs multi-line lines piecemeal, fish drops an early multi-line write, and any shell loses a line over 1 KB written early. * test(agent-launch): keep the real-zsh launch-line test out of the Windows lane's gate scan The Windows lane registration check read `const ZSH_PATH = process.platform === 'win32'` (the head of a multi-line ternary) as a Windows-true flag, so `describe.skipIf(!ZSH_PATH)` looked like a Windows-only suite. The file is POSIX-only; the zsh lookup is now a function. * refactor(protocol): move the agent.launch capabilities into their own module Main's protocol-version.ts sits at the 300-line cap, so the prompt-carry capability pushed it over. The four agent.launch capabilities and their doc move to agent-launch-runtime-capability.ts, re-exported by name and spread into RUNTIME_CAPABILITIES at the same position; the advertised lists and every export are unchanged. * refactor(protocol): import the agent.launch capabilities from their own module `export *` from protocol-version left the four names undefined under the mobile recording loader, which resolves a relative import through a Proxy with no own keys, so 37 phone recordings lost agent.launchReplay. Importers now name agent-launch-runtime-capability directly; protocol-version only spreads its list. * refactor(agent-launch): drop the unshipped viewMode field and trusted local caller id Both were inert in step 1 and existed only for step 2. agent.launch will become a public plugin API, so every wire field is permanent once shipped; a top-level viewMode reads as "choose terminal vs chat", which the host decides. Step 2 introduces placement and view intent under a placement object instead. * fix(agent-launch): read Codex's provisional startup from the rule files' hold anchor Main (stablyai#24375) moved Codex's provisional-header check into codex.json's provisional_startup hold anchor and deleted codex-terminal-readiness.ts, so the launch readiness hold now asks showsHoldAnchor, as main's own settled check does. * fix(agent-launch): hold rule-file name titles to quiet for a launch, and census the zsh fixture Main (stablyai#24375) answers a name-only title from each agent's rule file ahead of the sustained-title lane, so gemini.json's name_title settled a launch readiness wait on the shell's auto-title while Gemini was still booting. A launch now asks quiet of every weak idle verdict, as that lane did. Main's readiness census requires a recorder for every runtime fixture; the zsh prompt recording is a non-agent control. Gemini's synthetic baseline is regenerated for this PR's stated change: a bare gemini title is no longer its rest mark, so name-only rows settle weak, and a fresh working or blocked status is no longer overridden. * fix(agent-launch): paste a launch prompt only when the launched agent is proven in front A launch pasted its prompt unless a shell was proven in the terminal's foreground, so any read that could not prove one let the prompt through. After an agent exited at startup, its shell turned bracketed paste on at the next prompt, readiness fired on it, and the prompt was typed into the shell: - macOS: a pane runs its shell under login, so the process-group fence's root was never the shell's group and never proved it; the cached foreground name could also still name the exited process. - Windows Git Bash and WSL: the shell-alone-in-its-job check never answers. Now one fresh read of the terminal's foreground decides: agent, shell or unknown. Only 'agent' lets a write through (paste, Enter, second Enter, reused panes too); 'shell' still drops a ready signal. A Windows host never proves the agent, so there the launch line carries the prompt at any size, as on main. * test(agent-launch): cover the Windows QA stub, a grok override that exits at once * fix(agent-launch): keep the local socket alive while a prompted launch waits for its agent A launch with a prompt now waits up to 60 s for the terminal agent to be ready before it writes the prompt, and reports not-delivered when the agent never is. The local runtime socket closes a connection idle for 30 s unless the request is a long poll, so a launch whose agent exited at startup lost its reply and the caller saw 'runtime closed the connection' instead of not-delivered. Classify a prompted agent.launch and agent.launchReplay as a long poll, as orchestration.workerStart already is for the same wait. * refactor(agent-launch): narrow the launch params by 'in' instead of a cast * fix(agent-launch): find a launched agent behind a wrapper that leads its process group A tcsh or nu launch line runs the agent from /bin/sh '<script>', and a wrapper script that does not exec its agent does the same: the wrapper leads the terminal's foreground process group and the agent is a member of it. The fresh foreground read names the group's leader, sh, so a prompted launch was refused or pasted late (M4Air tcsh: 2 of 4 not delivered, 2 pasted ~9 s late). Before that read, take the host's process-group observation as positive proof when it names the launched agent among the foreground group's members and is younger than a ready signal's quiet window. It never proves a shell. * fix(agent-launch): judge the foreground-group proof by when its capture began, not how long ps took The age the host stamps on a process-group observation runs from the start of its whole-machine ps, so on a loaded Mac a capture begun after the read was asked for still read as older than 1 s and the proof was dropped. Count an observation whose capture began after the read was asked for, less the window a shared capture is reused across. * test(agent-launch): keep the crash-guard live test out of the Windows lane's gate scan The Windows-lane registration scan read the const assigned from a platform check as a Windows-only gate, though the suite runs everywhere but Windows; find zsh in a function instead, as the real-zsh typed-line test does. Under load the fresh foreground scan can fail to answer, which lets the shell's prompt settle readiness (2 of 4 paired runs). The guard still refuses that write, so assert the refused write, the property that must always hold. * perf(agent-launch): read a local pane's foreground from its own terminal, not the whole process table The foreground read that gates every launch paste ran the daemon's inspectProcess capture and then a fresh scan, each a whole-machine ps; the fresh one also waits for any capture already running before it starts its own. Measured here at load 5: 1.2 s a read (M4Air QA: 3.4-5.0 s, and worker starts 17.6-32 s against main's 9-12 s at load 25-84). On a local macOS or Linux host, take the pane's root pid from the provider's session inventory and run one ps limited to that pane's terminal. Its foreground process group decides: the launched agent or any non-shell member is the agent (a wrapper that did not exec its agent leads the group), a group of shells alone is the shell. Same pane, same verdict: 2.7 ms a read. SSH hosts keep the relay's observation and name. * test(mobile): re-measure the web app's script sweep after agent.launch's capabilities moved out of protocol-version The mobile web bundle check failed at 124 assets against a ceiling of 123. Main already sat exactly on that ceiling: its sweep table read 69 scripts at 16 routes while the tree builds 73, the whole margin of 4. This branch imports the agent.launch capabilities from their own module, so protocol-version is no longer pulled into the root layout and four other routes. That moves which routes share which modules, and the Qoder capability module, imported by protocol-version and the AI-vault resume path, no longer shares an importer set with anything, so it gets a chunk of its own: 74 scripts. The fence says to re-derive the bound rather than raise it, so the sweep is re-measured on this head (every prefix of the sorted route list). The worst route now adds 10 scripts (session), not 9, which moves the pinned shell crossing from 32 to 30 routes; main re-measured on its own lands on the same crossing. * fix(agent-launch): a worker's brief needs its agent found in front, and Grok's start answers on its composer A paired-server worker start whose agent exited at startup typed its brief into the server's shell, which ran it: the idle wait can settle on a shell back at its prompt, and the brief was written with no foreground read. Both worker-start paths now check before each brief write, as a launch prompt is checked: on a host that can find the agent in front it must be there; on one that cannot (Windows) a shell proven in front still refuses, and anything else writes as before. A Grok worker start waited ~10 s more than main: its only rest signal is its bare name, which a launch holds to quiet output, and Grok animates its logo for ten seconds after its composer glyph. A worker start for an agent whose rest signal is its bare name and whose composer draws a marker (Grok, DSH, mimo-code) now also answers on that marker, whichever comes first.
…an expired resend no longer blocks that text (stablyai#25149) * fix(mobile-chat): a returned message is appended to the draft, and an expired resend id is spent * test(mobile-chat): leave the idempotent append to the shared rule's own test * docs(native-chat): say a whitespace-only draft counts as empty * docs(mobile-chat): don't overclaim where returned text survives
stablyai#22294) * feat(native-chat): answer /context in chat view for Claude Over a terminal-backed chat, Claude's /context paints a grid only the hidden terminal sees and records nothing in the transcript, so the chat showed nothing. The chat host now answers it itself: the transcript decoder keeps each assistant response's API usage and model, and the composer replies with the same estimate the CLI statusline shows (input + cache tokens against the model's window) instead of typing the command into the PTY. The command is also listed in the curated Claude catalog so the slash menu offers it. * fix(native-chat): only state a /context window the host can establish The /context answer sized the window from the transcript's model id, which never carries the [1m] suffix, so a 1M session read as 200k (e.g. 450k / 200k, 225%). The window now comes from the session's tracked model resolved through the host CLI's own model listing (the resolvedModel field the list_models probe already returned), and only a [1m] id that matches the model the transcript last answered with is sized; anything else reports the used figure alone. Also: - Offer /context only in the desktop terminal-backed composer; the shared catalog is mobile's too, and mobile cannot answer it. - Answer a typed /context even with images attached, as the picker does, and keep the attachments armed instead of sending them to the terminal. - Report nothing until a response lands after a /compact or /clear sent from the chat, instead of the pre-compaction size. * test(native-chat): pin resolvedModel through Claude model discovery * fix(native-chat): say /context is unavailable when the host never reports usage A host that predates usage decoding, or a scraped view, decodes Claude's replies without a model or usage, so the chat promised an answer after the next response that would never come. Once the agent has answered and no answer names its model, say usage is not available for this session. * feat(native-chat): answer /context for OMP instead of Claude Claude and Codex answer /context themselves in the structured chat lane, so the terminal-backed Claude answer, its [1m] window rule, and the resolved-model plumbing through the model listing go away. OMP's /context only paints a panel inside its TUI. The OMP decoder now keeps the provider, model and usage of each reply (none for aborted or errored replies, which OMP's own gauge skips) and turns compaction rows into the existing compaction boundary. The OMP model listing keeps each model's context window, and the chat answers /context from the last reply's prompt size against the window of the provider/model that served it. * feat(native-chat): show OpenClaude's own /context report in chat OpenClaude writes its /context grid to the transcript as a <local-command-stdout> row, which the chat hid as harness noise. The live message preparation now turns the stdout row that answers an opted-in command (/context for OpenClaude) into command output with the ANSI colors stripped, and the notice row renders command output in monospace so the grid keeps its columns. Replies to other local commands stay hidden, and Claude is not opted in. OpenClaude's slash menu now lists /context; Claude's does not. * test(native-chat): pin OpenClaude /context output through live message preparation * test(native-chat): pin the OMP context window through model discovery * test(native-chat): pin monospace rendering of command output * test(native-chat): restore the notice row suite the command-output test replaced The previous commit rewrote NativeChatNoticeRow.test.tsx wholesale, deleting main's compaction, tone, plan, provider-notice and old-reader coverage. Restore it and pin monospace command output through the message row instead. * fix(mobile): keep OpenClaude /context off mobile, which cannot show its report The shared slash catalog now lists /context for OpenClaude because the desktop chat surfaces its transcript reply. Mobile's transcript fold does not, so the command did nothing there. Mobile now offers the catalog minus commands answered only by a surfaced reply, which returns its OpenClaude menu and send classification to main's behavior. * refactor(native-chat): drop the unread estimated flag from context usage Left from the earlier design; the answer text states the estimate itself. * fix(native-chat): keep /model replies hidden after an OpenClaude /context Skill surfacing ran first and rewrote non-catalog envelopes such as /model into plain user text, so command-output pairing no longer saw them. A later /model reply then paired with the earlier /context envelope and appeared in the chat. Pair outputs against the raw envelopes before skill surfacing. * fix(native-chat): word the OMP /context answer for a compacted session and show it as an aside After a compaction the agent has already answered, so the unknown-usage line now promises the next response instead. The host answer is one sentence, so it draws as the muted line it replaces rather than a monospace grid. The provider/model selector now comes from one helper shared by the OMP listing parser and the answer, and new tests drive real OpenClaude and OMP transcript lines through the decoders. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): pair command replies by row link and declare /context replies on the catalog A `<local-command-stdout>` row now answers the command row its `parentUuid` names, carried on decoded messages as the optional `parentId`, instead of the newest command row at or before its timestamp. A reply with no loaded linked row (older host, command outside the tail window) stays hidden. How each agent's `/context` is answered is declared once, as `reply` on its catalog row (`composer` for OMP, `transcript` for OpenClaude). The slash menu, the composer's answer, the transcript surfacing and the mobile catalog all derive from it, and both send paths share one intercept. Mobile no longer lists OMP `/context`, which did nothing there. * perf(native-chat): skip command-output surfacing for agents that declare no transcript reply Every Claude-format row now carries its parent link, so once a session holds any local-command reply (/model, /compact) the surfacing pass built a map of every message on each live update and changed nothing for agents like Claude. Agents whose catalog declares no transcript reply now return before the scan. --------- Co-authored-by: Claude <noreply@anthropic.com>
…d stop is joined instead of held (stablyai#24862) * fix(native-chat): a child's root exit is reported even during its close, and bookkeeping after it never reads as unproven - Both connections report the root process's exit once, with `expected` set when a close had begun. A close that came back unproven and whose root exits later is finished by the adapter, and its end reaches the host like any other. - A Claude close whose resume-point write fails after the exit was proven, and a Codex close whose terminal row is refused, now end the session and report the failure, instead of keeping a dead child indexed as if its exit were unproven. - A Codex close whose forced tree kill can't prove the descendants gone but saw the root exit reports the descendants and counts the root exit. - Every child exit with an identity, expected or not, is forwarded to the host. * fix(native-chat): the exit ends the child's record; an unfinished stop is the child's own close, which everyone joins - The host keeps no stored "stop still owed" record any more. A stop begins the child's close (`child.close`), which lives on the child and ends with it. A second Stop, the idle reaper, quit, a send and an option/answer/goal/rewind all join that close instead of retrying a separate obligation. - A caller waits on the close only as long as the step deadline; the close itself is never abandoned. A proof that lands after every caller stopped waiting reaches the host as the adapter's report of that exit, which ends the record through the same handler. - Once the exit is proven, draining, settling, the lease release and the adapter's acknowledgement are each attempted and reported on failure; none keeps the child on record. A start, and the handle's close, write a release that failed from this host's proof of that exit, so a failed write never refuses a send. - A start that meets a close still unverifiable is refused with `previousExitUnverifiable`, so the queued message is rejected with a send-again reason; nothing is held and nothing starts beside the old process. - The idle sweep goes back to idle reaping only. - Removes stablyai#24333's retry entry points, the wait row and its hold rule, the ask/failure cursors on the stored record, and the stop's own wake. Tests replace the stablyai#24333 unproven-stop test: a send joining an unproven close and an in-flight one, a late proof past the caller's bound, a root exiting after its close gave up, a proven exit whose resume-point write and lease release both failed, an unverifiable close rejecting the send and refusing an option change, a surviving descendant, quit and the idle reaper; and Codex's unverifiable, late-exit and joined-close cases. * fix(native-chat): a message refused because the old process's exit is unverifiable says so, and to send again The start failure for a refusal with reason `previousExitUnverifiable` reads "Orca couldn't confirm Claude's previous process ended. Send your message to try again." instead of "Claude couldn't restart." The status-row kind and the refusal reason stay in the shared lists for rows and hosts that still carry them; the catalogs keep one sentence for both. * fix(native-chat): a close's verdict is the root's exit alone, and what follows it is logged - A Claude close resolves as soon as the root's exit is proven: the session ends and its `ended` report goes out then. Saving the resume point runs afterwards and a failure is logged, so a slow or hung write never reads as an unproven exit or keeps a dead child on record. - A root that exits after its close came back unproven finishes that close through the same path as any close, so the session's child work is published as ended (background tasks and subagents no longer stay shown running for a dead agent), and a failure there is logged. - Codex logs a refused final row, and reports a root exit whose forced tree kill could not prove the rest of the tree gone the way Claude does, so the host logs it and blocks nothing. - Both adapters take the host's logger for this bookkeeping. * fix(native-chat): one handler ends every child's exit, and a join waits on the adapter's own close - One exit handler (`structured-agent-session-child-exit`) ends a child's record for an exit expected or not. `expected` only changes what the chat is told: the stop's cause, its end at the stop's ask, the settlement id, and no crash outcome row. The lease release keeps the exit's evidence; the handoff guard, lifecycle barrier, sink release and adapter acknowledgement apply to both. A Claude journal-sink failure ends in the same step as its stop, as Orca's own fault. - Joining a close is asking the adapter, whose close is memoized while it runs and bounded by its own kill escalation; the host keeps no attempt of its own and no 10 s caller bound. An ask after a close came back unproven runs the stop again. - A close's end is stamped where its stop was asked for (a repeated ask moves it), so the closed chat and failed start checks order a message accepted meanwhile after it. - A start refused because the old exit is unverifiable rejects what was queued in the same step. - The end of a close the host asked for no longer waits on the cross-session recovery chain. - The kill no longer waits for the stop event's write; the journal writes rows in order. * fix(native-chat): an exit's lease release lands whatever the length of its reason A crash's reason can carry kilobytes of the provider's stderr, and a lease whose death detail is over 512 characters fails the store's own check. The exit handler cut it, but the release a start or the chat handle's close re-derives did not, so after a crash whose own release failed every message was refused as not resumable until restart. The record's builder now cuts the detail to the record's bound, so no writer can hand it one too long. * fix(claude): a proven close waits at most 2 s for the output it already wrote Once the root's exit is proven, the close still waited for the SDK's output reader to end. Something outside the process tree that holds the output open would keep that close, and every send, Stop and quit joining it, waiting with no bound. The wait is now bounded; past it the close resolves as proven and the open output is logged. * fix(codex): an exit reported inside Orca's close keeps the reason Orca closed it for The connection reports the app-server's exit inside the close that ends it, so that report ended every Codex close and replaced the close's own reason (for example, a provider frame that could not be recorded) with the connection's stderr text in the ended record and the lease's exit evidence. The session now records Orca's close with its reason, and the exit it ends keeps that reason. The test connection reports its exit inside close the way the real one does. * fix(native-chat): quit stops delivery before it drains exit recovery Every exit now wakes delivery, and teardown drained exit recovery before it stopped delivery, so an exit settled in that window could start a fresh agent that teardown then killed. Teardown stops delivery first; queued messages wait for the next launch. * docs(native-chat): the unverifiable-exit refusal no longer names a caller's wait The caller's bounded wait was removed; the comment describes the close as it is now. * fix(native-chat): a stop whose kill did not take is logged, and the next ask kills again When a close's kill leaves the agent's root running, the host now logs it. Tests pin what a later ask does: each connection runs its whole stop again (Codex sends SIGKILL a second time), refuses input meanwhile, and proves the exit once the kill takes. * fix(native-chat): a start refused over the old process says Orca couldn't stop it The host reaches an unverifiable verdict only after its own kill left the agent's root running, on the machine that runs the agent, so the sentence now says that: "Orca couldn't stop {agent}'s previous process." The refusal reason, failure kind and wire shapes are unchanged. The host test also checks the failed kill is logged. * docs(native-chat): an unverifiable close verdict is a root that survived the kill The host's close runs where the agent runs, so lost contact never yields this verdict; the comment no longer says it does. * fix(native-chat): a kill that did not take is reported once, by whoever met it The log added at the close fired beside a Stop's own failure report for the same event. A stop still reports it through its failure; a send or option change refused over it now logs it at the refusal, the only place it is otherwise invisible. * test(native-chat): a second Stop joins a close the first could not prove and retries its kill * fix(native-chat): say a start refused beside an unstopped process plainly The rejection now reads "Couldn't stop {{agent}} from before. Send your message again to try once more." This kind has its own send-again step; every other failure keeps "Send your message to try again."
…tay in the activity view (stablyai#25376) * fix(sidebar): stop workspace reveals from leaving the activity view Reveals are skipped while the activity view is showing, and activation no longer lifts workspace-list filters there. Only explicit requests (Go to workspace, reveal-current-workspace, Cmd+J project row, agent send target, Cmd+1-9) switch to the workspace list via showSidebarWorkspaceList. * test(sidebar): keep test harnesses out of the sidebarBody writer census * feat(activity): workspace-origin filters and a matching options trigger Activity view gains its own persisted Hide automation-created, Hide CLI-created and Hide other-client agents toggles, applied through the existing activity scope filter so the selected-row exemption and identity-preserving return keep working. The options trigger now uses the workspace view's SlidersHorizontal icon and shared numeric badge, counting every active activity filter, and Add project stays visible so no header button shifts between views. * feat(activity): right-click menu on activity rows Open, Mark as read/unread, Go to workspace (real workspaces only), Copy (agent title, plus branch and path for real workspaces), and Clear from list for finished threads, reusing the row's existing actions and clearActivityThread. * fix(sidebar): leave the activity view only once activation commits Go to workspace and Cmd+1-9 switched the sidebar before activating, so a blocked folder activation still left the activity view. Activation now owns the switch via showWorkspaceList, applied after its failure gates. The separate showSidebarWorkspaceList action and the one-line sidebar-body-reveal module go away: setSidebarBody is the single writer, and the census pins its callers. * fix(activity): close the row preview when its right-click menu opens * fix(activity): keep the row preview closed while its right-click menu is open * fix(ui): give right-click menus the same solid surface as dropdowns * fix(activity): match the agent row menu to the workspace row menu * fix(activity): offer the same copy items as the workspace row menu
…yai#25638) reconcileReservationAccounting computed each cell's absolute reserved_requests from leases read before lockCellRows, so a host placed in that gap lost its unit: the counter went to 0 with one lease on the cell.
…ade (stablyai#23262) * test(terminal): restore the live fish fixture prerequisites (#24947) A restored pane waits for the initial status replay before subscribing to PTY output. This fixture never settled that replay, so fish printed its mode-2031 arm before the renderer connected. Its PTY API also omitted the reset-input listener required by the serializer, aborting attachment. Settle and dispose the existing startup-snapshot registration and provide the same reset-listener mock used by the other PTY tests. The real fish child-stdin assertions and timeouts remain unchanged. No production change. * fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640) Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes. Original-commit: f707cde14ab8cb2670b4076916cdfb7e28c79983 fix(shortcuts): defer TUI editing chords in terminal-first mode Original-commit: 0c6348e49e0d968e0170961d294a018adceaa9f3 docs(shortcuts): describe deferred preview terminal chords Original-commit: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Align worktree history shortcut metadata with terminal conflict policy Restacked-from: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Restacked-onto: f7b1f9d8be14d12244957ee0b710a3482ec62383 * Register supervised Qoder China and Qwen Code (#24616) * Add Qoder session history and search with real CLI coverage * Allow the real Qoder marker file to end with a newline * Keep Qoder tool output out of history previews and search * Keep Qoder search pages readable by older clients * Verify persisted Qoder history after a real generated and resumed task * Negotiate Qoder filters before searching an older execution host * Combine search client imports for the CI plugin gate * Keep the relay search oracle aligned with legacy agent filtering * Register supervised Qoder China and Qwen lifecycle integration * Cover Qoder China mobile assets and mixed-host resume gates * Verify Qoder provider tags against the older released wire parser * Verify China and Qwen keep independent Windows hook scripts * Verify Qoder registrations against the installed older Windows release * test(qoder): align search capability contracts and pin old-host fencing * fix(qoder): rank exact picker identities and command aliases first * test(qoder): preserve the regional CLI shared icon expectation Keep the full bundled-asset and no-remote-image checks, with an explicit shared-logo basename for Qoder China. The map also works with older catalog type unions. * fix(qoder): align China catalog entry with fallback order --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> * Use the measured pnpm lookup policy automatically in hosted root CI (#24951) * Select lookup automatically for the measured hosted root-install profile * Record hosted automatic-mode cold cache publication proof * Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692) * feat(antigravity): bridge IDE history into new CLI conversations * fix(antigravity): preserve fresh-launch model and environment for IDE references * fix(antigravity): forward IDE history opt-in through desktop IPC * fix(antigravity): rebuild remote IDE reference startup on its host * fix(antigravity): register IDE continuation action labels * fix(antigravity): confine IDE references and bound metadata reads * fix(antigravity): localize IDE continuation badges * Preserve scanner service cache assertions and refresh Antigravity opening metadata * Preserve Antigravity opening joins and target folder runtime authority * fix(opencode): retry timed-out SSH plugin updates (#24666) Preserve bounded retry behavior and the current-main status-envelope fields. Original-PR: #24124 Reviewed-source: 103144f9c5029f9217f64970271a5b7f69d3f823 Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com> * fix(opencode): keep Go credentials private and resolve backend keys (#24615) Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth. Original-topic-commit: 7903f1cddbc08fe1179f8bd964fff3dcb0e5d020 Original-topic-commit: 588117b5cf1dd57e2310c80068e3ddcc9f9044ec Original-topic-commit: dedd4f8c862a961393977dc3c2547f049391b509 Original-topic-commit: 6645dae104ee5271139fcc0e9564d9223b74835d Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-onto: b032867021c7ef1436ca606559e9d786580a84dc Co-authored-by: kespineira <kespineira@users.noreply.github.com> Co-authored-by: kevimux <kevimux@users.noreply.github.com> Reported-by: pullfrog Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f Native-helper-source: 80dbe23237db191c1e6280001c7e0563f3ca8846 Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c * Skip store notifications when refreshed work items are unchanged (#24530) An identical forced refresh previously returned an empty Zustand patch, creating a new root state and notifying every subscriber. It now returns the current state after renewing the existing freshness timestamp. Real row or metadata changes still publish once. * Read project activity once while sorting the sidebar (#24549) Reuse the existing pure recent rank once per actual entry tuple within each synchronous sort; discard the lookup after sorting. * Skip impossible link and tag matches in docs search results (#24646) * Skip impossible HTML matches when formatting docs search results After the existing link-removal phase, skip the unchanged HTML regex only when the current string has no closing delimiter. * Skip impossible link and tag matches in docs search results Skip the five unchanged link regexes when their protected-code input lacks ]; skip the unchanged HTML regex when its post-link input lacks >. * Decode complete transcript lines without copying their bytes (#24546) Decode each single owned Buffer synchronously; preserve concatenation for multipart records and remove a redundant tail array copy. * Clear unused speech worker timers after errors and exit (#24924) Call the existing idle timer cleanup when private current-worker state is cleared; keep current-worker guards, transcript/error order, deliberate warmth and stop deadline unchanged. * Reuse documentation search excerpts during result navigation (#24574) Memoize the existing pure excerpt renderer by complete raw text for the current result array, retaining original rendering as a miss fallback. * Append subagent handoffs without recopying their message list (#24672) Append each message reference to its private call-local scope list; retain the final merge and existing ordering. * Cancel plugin retry timers when their fetch is replaced (#24700) Reuse the existing timer clear block immediately after the fetch generation advances; preserve live retries and all state publications. * Avoid rebuilding visible file paths for single-row selection (#24743) Skip the visible path array only for keyboard replacement selection; the existing replacement branch never reads that order. * Reuse prepared reads while searching saved agent conversations (#24535) Use schema-complete session columns to enable the existing bounded statement cache without caching result rows. * Reuse discovered mobile chunks during static traversal (#24774) Iterate the existing live visited Set in FIFO discovery order instead of maintaining a second shifted queue; both actual callers own untouched esbuild JSON metadata. * Skip unused image-size calculations in mobile web browser requests (#24941) Use the existing mobile density budget only for mobile view; pass the existing constant to the existing assembler for web/default mode, where that argument is discarded. No cache, policy, request or native path changes. * Skip diff analysis after a result has been discarded (#24711) Move the unchanged pure render-limit calculation after both existing generation and section-token rejection fences. * Skip late browser grab toasts after their surface closes (#24727) Reuse the existing mounted-owner ref at the toast presentation entry while preserving all admitted extraction/screenshot and clipboard completion. * Classify native chat waiting messages once (#24771) Use the existing native filter traversal to populate a fresh waiting array, keeping the original predicate, policy, projection and output ordering while classifying each actual private slot once. * Skip discarded Kanban pruning for an empty selection (#24782) Guard only the existing open pruning branch when both its private selected Set and nullable anchor are empty; retain all original pre-guard projections and nonempty/anchor-only/public-helper behavior. * Index discovered test files once during shard selection validation (#24532) Verified selection plans previously validated each selected filename with a scan of all discovered files. One per-call Set now handles membership checks. Exact source/discovery checks, nonempty selection, fallback to all tests, shard balancing and manifests remain intact. * Clear the watchdog benchmark heartbeat when CPU sampling fails (#24802) Move the existing initial CPU observation and histogram enable inside the existing try/finally so their failures clear the sampler's owned heartbeat, preserving successful operation order and original errors. * Reuse the parsed notification when leasing a push delivery (#24645) * Reuse the parsed notification when leasing a push delivery Pass the notification already parsed for the dismissal check into the existing private delivery builder. * Reuse the parsed notification when leasing a push delivery Pass the notification already parsed for the dismissal check into the existing private delivery builder. * Update README downloads badge * Let retired-cache GC observations settle across the existing six-turn budget (#24967) * Collect test-selection evidence when full unit tests fail (#24955) * Collect advisory unit-selection evidence from failed full runs * Trigger checks after retargeting the evidence fix to main * Check each project repository once while filtering mobile cards (#24540) Reuse exact raw source/slug matching decisions within one project filter call, preserving the matcher, membership, negative matches, output order and identity. * Skip renderer callbacks after their request has ended (#24631) Reuse the existing pending-request identity check before starting its deferred renderer callback. * Decode single-piece saved-session tails without copying them (#24632) Decode the existing owned Buffer directly when the EOF tail has one piece; preserve the existing concatenation for multiple pieces. * Avoid irrelevant scroll-action searches in Mac snapshots (#24731) Check the current pure action name before searching for vertical scroll actions in the same immutable array. * Stop copying every retained browser page before attachment (#24553) Find the first page/generation match directly in the existing insertion-ordered Map instead of copying all page references into an array. * Reuse event byte sizes when relay watchers notify several clients (#24558) Store the existing grouped numeric byte-size array on the already call-local watcher batch sizing object, for reuse by later chunking clients. * Stop building unused import candidates in the test planner (#24611) Replace the existing ordered extension map/find with a loop that forms each candidate only when its existing lookup is reached. * docs(terminal): explain local macOS/Linux shell startup files Document the actual login/interactive shell startup-file order and existing shell setup behavior. Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai> * fix(editor): recognize Ruby task and configuration files Add Ruby task/configuration filenames to the existing generated language associations. Co-authored-by: ggbdpq <ggbdpq@gmail.com> Co-authored-by: Neil <neil@stably.ai> * Speed up large Markdown Find and render oversized tables (#24948) * Speed up large Markdown Find and render oversized tables * Poll table preview geometry outside hidden renderers * Preserve Markdown navigation through refresh and tab restoration * Confirm Markdown restoration when refreshed content is ready * Trace table refresh positions and update Unicode search reference * Recognize queued measurement scrolls before restoring Markdown anchors * Rebuild Markdown Find ranges after renderer components change * fix(source-control): generate clean OpenCode messages locally and over SSH (#24613) * fix(source-control): generate clean OpenCode answers on local and SSH hosts Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes. Original-commit: 64bb15e3f20799a21f108c849b0bdff83c692712 fix(source-control): generate clean OpenCode answers on local and SSH hosts Use configured models and JSON answer/error events, preserve run-first arguments, and handle the precise v2 variant rejection. Hydrate SSH execution-host PATH through the existing bounded login environment resolver before direct spawning. Credits: andy-murr (PR #5197 SSH environment intent) and coelho-doti (PR #13065 argument-order intent). Original-commit: 1b60ec5d11132a5fed0695faea5de782a228a8e5 fix(source-control): retry inline OpenCode model and variant options Original-commit: 98fdecc7a33ef1571dbf2854f309e4bfa1ec4300 Preserve OpenCode named errors without a data message Restacked-from: 98fdecc7a33ef1571dbf2854f309e4bfa1ec4300 Restacked-onto: f7b1f9d8be14d12244957ee0b710a3482ec62383 * fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776) * fix(ci): run mobile typechecks without concurrent dependency refresh * test(ci): check effective Linux E2E package list * test(ci): preserve the mobile production compiler barrier --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> * test(terminal): restore the live fish fixture prerequisites (#24947) A restored pane waits for the initial status replay before subscribing to PTY output. This fixture never settled that replay, so fish printed its mode-2031 arm before the renderer connected. Its PTY API also omitted the reset-input listener required by the serializer, aborting attachment. Settle and dispose the existing startup-snapshot registration and provide the same reset-listener mock used by the other PTY tests. The real fish child-stdin assertions and timeouts remain unchanged. No production change. * fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640) Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes. Original-commit: f707cde14ab8cb2670b4076916cdfb7e28c79983 fix(shortcuts): defer TUI editing chords in terminal-first mode Original-commit: 0c6348e49e0d968e0170961d294a018adceaa9f3 docs(shortcuts): describe deferred preview terminal chords Original-commit: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Align worktree history shortcut metadata with terminal conflict policy Restacked-from: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Restacked-onto: f7b1f9d8be14d12244957ee0b710a3482ec62383 * Register supervised Qoder China and Qwen Code (#24616) * Add Qoder session history and search with real CLI coverage * Allow the real Qoder marker file to end with a newline * Keep Qoder tool output out of history previews and search * Keep Qoder search pages readable by older clients * Verify persisted Qoder history after a real generated and resumed task * Negotiate Qoder filters before searching an older execution host * Combine search client imports for the CI plugin gate * Keep the relay search oracle aligned with legacy agent filtering * Register supervised Qoder China and Qwen lifecycle integration * Cover Qoder China mobile assets and mixed-host resume gates * Verify Qoder provider tags against the older released wire parser * Verify China and Qwen keep independent Windows hook scripts * Verify Qoder registrations against the installed older Windows release * test(qoder): align search capability contracts and pin old-host fencing * fix(qoder): rank exact picker identities and command aliases first * test(qoder): preserve the regional CLI shared icon expectation Keep the full bundled-asset and no-remote-image checks, with an explicit shared-logo basename for Qoder China. The map also works with older catalog type unions. * fix(qoder): align China catalog entry with fallback order --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> * Use the measured pnpm lookup policy automatically in hosted root CI (#24951) * Select lookup automatically for the measured hosted root-install profile * Record hosted automatic-mode cold cache publication proof * fix: bound remote generation setup and honor OpenCode option terminators Count execution-host profile resolution inside the existing request deadline, cancel its waiter promptly, and pass only the remaining time to the child. Shared bounded profile probes keep their existing cache lifetime; the SSH transport margin is unchanged. Read OpenCode output format from active final-argv options before -- so literal prompt arguments cannot select the JSON finalizer. Fresh exact-source controls reproduce nine failures before; 139 related checks pass after, including primary/fallback delays, deadline boundaries, cancellation, parser metadata, and SSH lanes. Node typecheck and strict changed-file lint pass. * Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692) * feat(antigravity): bridge IDE history into new CLI conversations * fix(antigravity): preserve fresh-launch model and environment for IDE references * fix(antigravity): forward IDE history opt-in through desktop IPC * fix(antigravity): rebuild remote IDE reference startup on its host * fix(antigravity): register IDE continuation action labels * fix(antigravity): confine IDE references and bound metadata reads * fix(antigravity): localize IDE continuation badges * Preserve scanner service cache assertions and refresh Antigravity opening metadata * Preserve Antigravity opening joins and target folder runtime authority * fix(opencode): retry timed-out SSH plugin updates (#24666) Preserve bounded retry behavior and the current-main status-envelope fields. Original-PR: #24124 Reviewed-source: 103144f9c5029f9217f64970271a5b7f69d3f823 Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com> * fix(opencode): keep Go credentials private and resolve backend keys (#24615) Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth. Original-topic-commit: 7903f1cddbc08fe1179f8bd964fff3dcb0e5d020 Original-topic-commit: 588117b5cf1dd57e2310c80068e3ddcc9f9044ec Original-topic-commit: dedd4f8c862a961393977dc3c2547f049391b509 Original-topic-commit: 6645dae104ee5271139fcc0e9564d9223b74835d Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-onto: b032867021c7ef1436ca606559e9d786580a84dc Co-authored-by: kespineira <kespineira@users.noreply.github.com> Co-authored-by: kevimux <kevimux@users.noreply.github.com> Reported-by: pullfrog Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f Native-helper-source: 80dbe23237db191c1e6280001c7e0563f3ca8846 Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c * fix(opencode): enforce deadline through executable startup Keep synchronous Windows PATH resolution and child startup within the existing request budget. --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com> * fix(persistence): preserve projectGroupOrder on restart for flat folder-scan groups Retain saved project ranks when the project remains in its folder-scan group. Co-authored-by: lurunzi <lurunzi@gmail.com> Co-authored-by: Neil <neil@stably.ai> * fix(runtime): reclaim temp files orphaned by interrupted mobile store writes Reuse the existing stale temporary-file cleanup policy when each mobile store opens. Co-authored-by: LDH1103 <ldh517525@gmail.com> Co-authored-by: Neil <neil@stably.ai> * fix(terminal): show actionable copy for missing folder workspace paths Show the existing folder-path failure with concrete recovery instructions. Co-authored-by: Wayn_Liu <wayntingliu@gmail.com> Co-authored-by: Neil <neil@stably.ai> * docs: reference local orca.yaml and .worktreeinclude Document the existing workspace configuration, include/copy rules and sharing behavior. Co-authored-by: Neil <neil@stably.ai> * fix(orchestration): allow model selection for OMP workers Pass an explicitly requested model through the existing OMP worker launch catalog. Co-authored-by: Neil <neil@stably.ai> * fix(cli): preserve primitive success results in JSON output Check for an object before inspecting screenshot fields so primitive success results remain printable. Related: https://github.com/stablyai/orca/pull/14735 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com> * Show loaded file changes before deleting a workspace Expose up to ten already loaded changed paths without altering deletion counts, hydration or authorization. Related: https://github.com/stablyai/orca/pull/22778 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Michiel de Gooijer <mdgooijer@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(keybindings): record and match Option+digit shortcuts on macOS Use the physical digit key for explicitly assigned Mac Option+digit shortcuts while retaining modifier checks. Co-authored-by: marcuslannister <marcus@lannister.cc> Co-authored-by: Neil <neil@stably.ai> * fix(editor): don't throw closing a stale active file Recover stale active-file selection within the owning workspace before choosing a surviving editor. Related: https://github.com/stablyai/orca/pull/24715 Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Neil <neil@stably.ai> * Fix Project Settings targeting for multiple local checkouts Carry the selected checkout identity into the existing project Settings action. Co-authored-by: fsmeier <1506919+fsmeier@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> * feat(documents): open CSV and TSV files from the OS Extend existing OS document associations and delivery to CSV/TSV, preserving restoration and authorization. Co-authored-by: Neil <neil@stably.ai> * feat(cli): link GitHub and GitLab items on worktree create and set Expose and validate the existing workspace link fields, preserving omitted values and provider identity checks. Related: https://github.com/stablyai/orca/pull/22609 Co-authored-by: marco song <marco.song@mvlchain.io> Co-authored-by: SongMarco <20613630+SongMarco@users.noreply.github.com> Co-authored-by: Luca Critelli <lucacri@gmail.com> Co-authored-by: Neil <neil@stably.ai> * feat(sidebar): include folder workspaces in keyboard navigation Use the rendered sidebar row order and host identity when cycling through folder and Git workspaces. Related: https://github.com/stablyai/orca/pull/10555 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: JeongUk Park <jeongph.dev@gmail.com> * fix(build): turn off MSBuild file tracking for Windows native rebuilds Default Windows native rebuilds to TrackFileAccess=false while preserving explicit caller preferences. Co-authored-by: B1nh M1nh <43268322+b1nhm1nh@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> * Fix Ctrl+M in Linux and Windows terminals Keep the Minimize menu item but disable its accelerator registration on Linux and Windows. Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com> Related contribution: https://github.com/stablyai/orca/pull/24143 * fix(startup): read a nushell login PATH from $env.PATH Use Nushell login command syntax and preserve the actual login PATH value. Related: https://github.com/stablyai/orca/pull/22677 Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com> Co-authored-by: Neil <neil@stably.ai> * fix(cursor): run local hooks through sh for non-POSIX login shells Keep POSIX hook syntax inside one quoted sh command so the login shell can invoke it safely. Related: https://github.com/stablyai/orca/pull/22663 Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Neil <neil@stably.ai> * Fix word wrap for both panes in side-by-side diffs Forward wrapping to both diff panes through the existing editor option path and clean up listeners. Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Neil <neil@stably.ai> * fix(monaco): highlight Svelte block closers inside markup Return Svelte block closers to the existing markup tokenizer state. Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Neil <neil@stably.ai> * fix(repos): keep active clone dialog open on outside clicks Ignore accidental outside dismissal only while cloning; Escape, Close and Back still cancel. Related: https://github.com/stablyai/orca/pull/24581, https://github.com/stablyai/orca/pull/23430 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance> * fix(editor): keep chat visible after closing Markdown tabs Count remaining unified chat tabs before clearing workspace selection during editor close. Related: https://github.com/stablyai/orca/pull/24273, https://github.com/stablyai/orca/pull/23760 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Honor typed starting numbers in chat ordered lists Forward the existing parsed ordered-list start attribute to both chat Markdown renderers. Related: https://github.com/stablyai/orca/pull/19765 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Frederic Barthelemy <git@fbartho.com> * Normalize base source once while checking changed-code diagnostics (#24557) Reuse the exact existing moved-code matching algorithm with run-local lazy normalization of immutable base source blocks across diagnostics. * Support real OpenCode sessions in native Chat (#24647) * Use bounded OpenCode context for vault session continuation OpenCode database and synthetic row paths are not text transcripts. Use the vault preview or captured pane context, preserving actual transcript paths containing a hash and supporting both OpenCode lanes and Windows paths. Adapted the intent of #11859 and extended it to actual installed v2 vault rows. Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com> * Read real OpenCode sessions in terminal-backed native Chat Reuse the bounded AI Vault SQLite worker for v1 and v2 session pages and live updates. Keep terminal input as the real execution path and pace OpenCode Stop through its two-Escape interrupt. Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com> * fix(opencode): publish approval cards for permission requests * Send OpenCode native approval through its Enter selector * Resolve mobile Chat readability for folder workspaces * Bound OpenCode part batches and preserve v2 image attachments * Prefer live migrated OpenCode sessions over legacy copies * Consolidate mobile Chat eligibility test imports * Consolidate OpenCode SQLite protocol type imports * Update native chat settings contract for both OpenCode agents * fix(native-chat): reconcile bounded OpenCode transcript reads * fix(native-chat): dispatch OpenCode questions safely * fix(native-chat): keep native discovery and transcript windows current * feat(accounts): link standalone GLM Coding Plans (#24618) * feat(accounts): link standalone GLM Coding Plans Adapt the reviewed GLM accounts contribution to current main, retain Antigravity behavior, guard late credential results, expose storage protection, and redact quota errors. Co-authored-by: Luchong <lu740528977@gmail.com> * fix(accounts): retain GLM credential results during quota refresh * fix(accounts): make GLM credential editing desktop-only * fix(accounts): mirror the host GLM site in paired clients * fix(accounts): report unknown GLM host details and split web settings tests Apply the independently reviewed Accounts correction from697284a without the v2 adapter commits. Preserve the saved-key store and serialized write behavior. * fix(zcode): ship required GLM account translation entries * chore: record GLM reconciliation hook validation * chore: validate installed GLM commit hooks * test: complete GLM account fixtures and web API inventory --------- Co-authored-by: Luchong <lu740528977@gmail.com> * fix(native-chat): route transcript requests through shared SQLite worker * fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776) * fix(ci): run mobile typechecks without concurrent dependency refresh * test(ci): check effective Linux E2E package list * test(ci): preserve the mobile production compiler barrier --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> * test(terminal): restore the live fish fixture prerequisites (#24947) A restored pane waits for the initial status replay before subscribing to PTY output. This fixture never settled that replay, so fish printed its mode-2031 arm before the renderer connected. Its PTY API also omitted the reset-input listener required by the serializer, aborting attachment. Settle and dispose the existing startup-snapshot registration and provide the same reset-listener mock used by the other PTY tests. The real fish child-stdin assertions and timeouts remain unchanged. No production change. * fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640) Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes. Original-commit: f707cde14ab8cb2670b4076916cdfb7e28c79983 fix(shortcuts): defer TUI editing chords in terminal-first mode Original-commit: 0c6348e49e0d968e0170961d294a018adceaa9f3 docs(shortcuts): describe deferred preview terminal chords Original-commit: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Align worktree history shortcut metadata with terminal conflict policy Restacked-from: be62c1b6c6b305cb8091161c1ccdd943a6e1d470 Restacked-onto: f7b1f9d8be14d12244957ee0b710a3482ec62383 * Register supervised Qoder China and Qwen Code (#24616) * Add Qoder session history and search with real CLI coverage * Allow the real Qoder marker file to end with a newline * Keep Qoder tool output out of history previews and search * Keep Qoder search pages readable by older clients * Verify persisted Qoder history after a real generated and resumed task * Negotiate Qoder filters before searching an older execution host * Combine search client imports for the CI plugin gate * Keep the relay search oracle aligned with legacy agent filtering * Register supervised Qoder China and Qwen lifecycle integration * Cover Qoder China mobile assets and mixed-host resume gates * Verify Qoder provider tags against the older released wire parser * Verify China and Qwen keep independent Windows hook scripts * Verify Qoder registrations against the installed older Windows release * test(qoder): align search capability contracts and pin old-host fencing * fix(qoder): rank exact picker identities and command aliases first * test(qoder): preserve the regional CLI shared icon expectation Keep the full bundled-asset and no-remote-image checks, with an explicit shared-logo basename for Qoder China. The map also works with older catalog type unions. * fix(qoder): align China catalog entry with fallback order --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> * Use the measured pnpm lookup policy automatically in hosted root CI (#24951) * Select lookup automatically for the measured hosted root-install profile * Record hosted automatic-mode cold cache publication proof * fix(native-chat): keep OpenCode history usable at read limits Continue past failed database probes while preserving discovery cancellation. Verify rows displaced by a capped tail before deciding whether to replace history. Represent oversized v1/v2 rows with the existing omission text and stable cursors. * Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692) * feat(antigravity): bridge IDE history into new CLI conversations * fix(antigravity): preserve fresh-launch model and environment for IDE references * fix(antigravity): forward IDE history opt-in through desktop IPC * fix(antigravity): rebuild remote IDE reference startup on its host * fix(antigravity): register IDE continuation action labels * fix(antigravity): confine IDE references and bound metadata reads * fix(antigravity): localize IDE continuation badges * Preserve scanner service cache assertions and refresh Antigravity opening metadata * Preserve Antigravity opening joins and target folder runtime authority * fix(opencode): retry timed-out SSH plugin updates (#24666) Preserve bounded retry behavior and the current-main status-envelope fields. Original-PR: #24124 Reviewed-source: 103144f9c5029f9217f64970271a5b7f69d3f823 Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com> * fix(opencode): keep Go credentials private and resolve backend keys (#24615) Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth. Original-topic-commit: 7903f1cddbc08fe1179f8bd964fff3dcb0e5d020 Original-topic-commit: 588117b5cf1dd57e2310c80068e3ddcc9f9044ec Original-topic-commit: dedd4f8c862a961393977dc3c2547f049391b509 Original-topic-commit: 6645dae104ee5271139fcc0e9564d9223b74835d Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-onto: b032867021c7ef1436ca606559e9d786580a84dc Co-authored-by: kespineira <kespineira@users.noreply.github.com> Co-authored-by: kevimux <kevimux@users.noreply.github.com> Reported-by: pullfrog Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f Native-helper-source: 80dbe23237db191c1e6280001c7e0563f3ca8846 Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c --------- Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com> Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com> Co-authored-by: Luchong <lu740528977@gmail.com> Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example> Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com> * Drain removal fixture jobs before resetting and deleting their records (#24977) * Reuse measured Electron preparation for current Terminal Perf refs (#24968) * feat(jcode): add Jcode as a supported TUI agent with managed hooks Ports PR #10521 onto current main: agent catalog, managed hook service, agent-status listener, session resume, AI Vault parser, per-pane daemon isolation, and Source Control AI support. Co-authored-by: Neil <neil@stably.ai> * feat(jcode): evidence-backed status pipeline (turn_start, live pre_tool, questions) Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): register vault fixture, dynamic model discovery, script refresher Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): keep finished-turn detail so completion notifications fire Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): show the prompt in agent rows instead of jcode's repainting title Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): pre-warm the per-pane daemon so a cold runtime dir cannot time out Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): stop the OSC color skip from crashing every pane connect Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * refactor(jcode): fold three reverse-scan copies into one, reuse the shared hook POST The jcode journal reader, the Claude transcript reader and the Command Code transcript reader each carried their own copy of the same reverse chunked line scan; they now share one tested helper. jcode's managed hook script drops its hand-rolled curl for buildPosixAgentHookPostCommand, which also gains it the raw-JSON transport and the --noproxy guard the bespoke copy was missing. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): narrow dynamic reads with predicates, pin the Windows hook shape CI's anti-slop audit rejects Reflect.get: parse dynamic input into a named type instead. Adds Windows script-shape tests too, since Windows is the platform this change could not be exercised on directly. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * test(jcode): measure the gate against the synchronous path, not the clock The absolute 1s bound was the flake CI shard 3/8 hit: it is tight enough to catch a synchronous gate on an idle laptop and too tight on a loaded runner. Measuring the same POST both ways on the same machine makes the claim a ratio, which is what the test is actually about. Mutation-checked: a synchronous gate reads 6120ms against a 1517ms observer. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * test(jcode): drop the wall-clock gate assertion The bound was machine-speed sensitive and flaked on CI shard 3/8 at 1525ms. The structural assertions (detached gate branch, foreground observer branch) and the no-hang stdin drain cover the same contract without a timer. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): address CodeRabbit review on #22539 - Windows posted no payload at all: the shared builder reads `payload@-` from stdin, which the gate has already drained and observer hooks never receive, so every Windows event was dropped. Write the env var to a temp file and pipe it. - The daemon pre-warm never fired for daemon-host spawns, which is the default local path; it now runs there too, and from the final env so the daemon gets the hook port and token. - A failed runtime-dir mkdir took down every local terminal, jcode or not. - removeJcodeManagedHooks matched the raw line, so a user hook whose comment mentioned the managed script was deleted; matching on Windows never worked. - A managed entry left by a copied home or a platform switch is now repointed instead of being reported as user-owned forever. - The OSC colour skip only checked launchAgent, so a command- or telemetry-named jcode pane still leaked the reply into its composer. - `['hooks']` and a commented scalar are recognised, instead of appending a second [hooks] table that makes jcode reject the whole config. - A failed tool's error is marked as tool output rather than agent prose. - The vault keeps a session's stored name, counts its tokens, and skips background_task and [Scheduled task] turns. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): read the journal once per turn, key prompts by byte offset The jcode prompt reader ran a synchronous bounded file scan plus a JSON parse on every hook event. jcode blocks on pre_tool, so a turn that ran four tools charged the user eight scans of latency it did not need — the prompt cannot change inside a turn. - Cache the journal read per pane, refreshed on the turn boundary that can change it. The cache holds the whole evidence record, since hasExplicitUserPrompt needs the transcript-evidence flag and not just the text, and it joins the existing pane-scoped lifecycle (close, rename, reset) rather than living in a module singleton. - Key a journal prompt by its absolute byte offset instead of its region-local line index. The backward scan windows the file from EOF, so appending shifted every boundary and reminted the key for a prompt that never moved; a repeated turn_end then slipped past the same-hash dedupe as a second done event with duplicate telemetry. - Pin the platform in the daemon pre-warm tests. The pre-warm is a no-op off POSIX, so the dedupe and retry cases would have passed vacuously on a Windows runner. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): quote the managed hook path, drop tools from patch prompts Three fixes, all on paths this PR could not exercise locally. The managed hook command was stored as a bare path. jcode tokenizes that string shell-style before exec'ing it directly (parse_hook_command, crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and every unquoted backslash is consumed as an escape. So on Windows `C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as `C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a POSIX home with a space split into two arguments. Store the path single-quoted (verbatim, backslashes included), falling back to double quotes for a path containing a single quote. Existing bare entries are already repointed by the stale-key path, and getStatus accepts both forms so the repair is not reported as a user-owned hook. The quoting helper was previously dead code that only tests called; the three production sites now use it. isJcodeManagedCommand also normalizes separators, since a `/`-only needle never matched a Windows entry. Commit-message generation feeds a staged patch to `jcode run` as the prompt — attacker-influenced text — while jcode's default profile exposes shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to an empty allowed-tool set in jcode's config (base_allowed_tools), matching the read-only posture claude (plan) and codex (read-only) already take. docs/reference/jcode-hook-events.md was never actually in this PR: the repo ignores docs/** and tracks reference docs by allow-list only, so the captured-payload evidence four source comments point at was silently dropped. Allow-list it. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * test(jcode): pin the tool-profile flag in the generation plan too The argv assertion lives in two places; --tool-profile none only landed in one, so the plan test still expected the unrestricted argv. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(text-generation): refuse an oversized argv prompt on Linux too The pre-spawn size guard only ran on Windows. Linux caps a single argv entry at MAX_ARG_STRLEN (32 pages, 128 KiB on a 4-KiB-page host) and execve fails with E2BIG past it, so an agent that delivers the whole prompt as one argument — jcode, and the other argv-delivery agents — failed on a large staged diff with an error the user could not act on. The cap is per-argument and in bytes, which is why it is not the Windows line budget: 40k chars trips Windows and is nowhere near the Linux limit, so folding them together would have refused prompts Linux runs fine. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): register in main's remote-installer guard, drop our duplicate Rebasing onto 853 commits of main surfaced two things the earlier branch had hidden. main already owns a guard for the issue-#7253 bug class (`remote-hook-service-registry-coverage.test.ts`). This branch had added a second, near-identical one — a parallel implementation of a test that already existed, which is what AGENTS.md's reuse rule is about. Deleted ours and registered jcode in main's, which is the one that has kept pace with every agent added since. Also fixes a missing separator in the mobile icon map. `pnpm tc` does not cover `mobile/`, so only the session-route closure suite caught it. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * fix(jcode): decompose the four files jcode pushed over max-lines Adding an agent tipped four modules past their line budget. AGENTS.md forbids a `max-lines` disable or a per-file bump, so each is split on a real seam rather than silenced: - agent-catalog.tsx keeps `AgentIcon`, which 70+ files import, and the rows move out. The rows alone exceed the 300-line budget a `.ts` file gets, so they follow the primary/secondary split this repo already uses for commit-message agent specs. - getAgentResumeArgv -> agent-resume-argv.ts, re-exported so the 18 call sites keep one import path. - isDiscoverableSessionFile/pathSegments -> session-file-discovery.ts. - remoteCodexSources -> remote-session-scanner-codex-sources.ts; Codex is the one remote agent with two CODEX_HOME roots. Also: - Records the readiness-census baseline jcode now needs. main added that gate while this branch was out; the fixture is the recorded 74-case matrix, not a hand-written one. - Restores two entries a rebase resolution silently dropped from config/tsconfig.cli.json (gitlab/project-ref-parser, startup/shell-path-probe). Nothing to do with jcode; losing them was a conflict-resolution mistake on this branch. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com> * feat(native-chat): open structured chats on the paired Orca server that owns the workspace (#24205) * fix(native-chat): a host admits structured sessions by client capability, not its own chat setting A host's experimentalStructuredNativeChat decided whether any paired client could reach agentSession.* at all, and whether session.tabs.* showed it structured tabs. That setting is the host user's own launch preference: whether a new agent opens as a chat or a terminal is decided by whoever launches it. Using it as admission control meant a client whose own preference was "structured chat" was refused on a host whose preference was "terminal", and chats opened while the setting was on were withheld from mobile once it was turned off. The gate now asks one thing: did the client advertise agent-session.structured.v1 (in-process callers negotiate nothing and are always admitted). Tab projection and restore follow the same rule. With the setting no longer gating anything, the separate cleanup gate (close, cancel, unsubscribe, release), which existed only so those kept working after the setting was switched off, is identical to the main gate and is folded into it. The settings listener that republished tabs when the setting changed is removed, since projection no longer depends on it. The host setting still picks the default for launches that start on the host itself (agent.launch from mobile, orchestration worker-start). * fix(native-chat): the desktop declares structured chat support to paired hosts The desktop renderer advertised agent-session.structured.v1 (and the Claude, turn-item and background-task capabilities that go with it) to its own main process but not to a paired Orca server. The server therefore refused every agentSession.* call from the desktop and stripped structured chat tabs out of the tab list it published to it, so a structured chat running on a paired server never appeared on the desktop, even though the renderer already mirrors a host's agent-session tabs and drives each one against the server that owns its workspace. The same renderer reads structured chats on either host, so the remote Electron list now carries the same structured-session capabilities as the local one, and the capability test pins that nothing is advertised only locally. * feat(native-chat): open structured chats on the paired server that owns the workspace With the structured-chat default on, an agent launched in a workspace that lives on a paired Orca server always opened as a terminal (or the terminal-backed chat view). Three things kept it off the structured path: the launch check refused every host but this machine, a remote workspace was handed to the host-published terminal path before the structured route was even considered, and the structured launch pipeline sent create and every follow-up call to this machine's runtime. A workspace's owning runtime is fixed, so the pipeline now derives it from the workspace instead of assuming this machine (structured-agent-session-owner.ts, the same derivation the chat pane already uses to read a session). The launch intent carries that target; the pre-create support check, create, the publication check and fence read, the launch prompt send, held option picks, the "focus this chat" marker, the placeholder tab's host, and tab close/purge all use it. The launch check now accepts a paired server and asks that server's own capabilities (read from the status the client already cached for it) rather than this machine's. An SSH workspace stays terminal-backed: no Orca runtime runs there. The host still answers createSupport before anything is created, so an older server that refuses shows the failure in the chat tab. A chat the user closed before its create landed is now also retired on the paired server when it publishes, as the local sync already does. Orchestration workers placed on another runtime are unchanged: federation creates terminal agents only. * fix(native-chat): negotiate client-chosen launch mode so released phones and old servers keep terminals Hosts advertise agent-session.structured.client-launch-mode.v1: they admit structured sessions by client capability alone. A remote client that does not advertise it (phones released before agent.launch) asks createSupport to pick the launch mode, so the host keeps answering that with its own setting, exactly as before. Cleanup methods keep their own named gate so a future admission condition cannot make close or cancel refusable. * refactor(runtime): keep the Electron client capability list in its own module protocol-version.ts is at its line budget; the list is what the desktop advertises to paired hosts, not the host's own contract. * fix(native-chat): the desktop declares it picks each launch mode itself Paired hosts and the desktop's own main process then answer createSupport by the workspace rather than by their own chat setting. * fix(native-chat): pin each structured chat to the host it was launched on - Route: a paired server opens a chat only when it advertises the client-chosen launch mode; an older server keeps its terminal. Its capabilities come from the store's host status, not the compatibility cache that is empty after boot or reconnect. - A launch command override is this machine's: the route applies it only locally, and a host's createSupport refuses on its own override. - The owning host is resolved once, from the same value the route used, and carried on the launch intent, its persisted record (legacy records load as local), the provisional tab and every mirrored chat tab. Close, purge, retry and reload read it instead of re-deriving it from a worktree id two hosts can share; an owner that cannot be named refuses. - Cancellation tombstones record their host: only that host's authoritative inventory retires one, restored cleanup closes it there, and a paired host's tombstone expires after 30 days if it never answers. - A paired server's frame settles launches it published, as the local inventory already does for this machine. * fix(native-chat): a paired server that declines a chat opens its terminal instead createSupport only reads, so both of its non-answers are settled before anything is created: - A paired server that answers it cannot run the chat (a WSL repo, a Claude account mismatch, its own launch command override) closes the chat tab and opens the terminal the route would have chosen, with a notice saying why. This machine's own decline stays a failed chat. - A host that could not be asked closes the chat tab and leaves one failure toast, instead of a lingering "could not confirm" chat. * test(native-chat): a provisional chat carries its launch's host and hands pre-create failures on * chore(native-chat): justify the two type assertions this change's lines touch * test(native-chat): state why each staged test fixture is cast * fix(native-chat): chats that already exist keep showing whatever the chat setting says The structured chat setting decides only what new agents open as. With it off, this machine's structured chats used to be hidden while the host, which no longer reads the setting, still reported them to the workspace activation gate, so a workspace holding only a chat opened empty. The local chat mirror and its startup restore now run whatever the setting says, the continue-after-restart offer follows the chats that exist, and the setting's copy says it applies to new agents. * fix(native-chat): the browser client keeps its host terminal on paired servers A browser client whose own preferences turn structured chat on took the structured route for every paired-server workspace, but its handshake never says it reads structured sessions, so the server refused the chat and the user got a failed chat tab where a host terminal used to open. The route for a paired host now also asks what this client advertises to it: the desktop's list does, the browser client's does not. Its handshake list is now a named constant the route reads, so the two cannot drift. The chat setting's copy now says it runs on paired Orca servers too; WSL and SSH hosts still use terminal chat. * fix(native-chat): a retried launch a paired server declines opens its terminal too A launch restored after a reload settles only through its Retry, so a declining paired server left a failed chat there while a first launch got the server's terminal and a notice. The chat's Retry now hands the same pre-create failures to the same replacement, carrying the prompt the launch had staged. * refactor(native-chat): a paired host's cancelled-chat record ends on its 30-day TTL The paired census re-read a host's whole inventory after every authoritative frame to retire tombstones, and a tombstone restored after a reload needed a second such frame, so in practice it retired nothing. A tombstone guards a random session id and is inert once stale; the chat is already closed on its host whenever a frame shows it. The census, its trigger in the mirror layer and its cleanup are removed; the owner-scoped tombstones, close-on-sight, the TTL and publication marking from frames stay. * fix(native-chat): a chat's pane and status read from the host recorded on its tab The chat pane and its sidebar status still derived the host from the workspace id, which two hosts can share; a paired chat in a non-active same-id workspace was read from this machine. Both now read the owner stamped on the tab, as close, purge and publication already do. * fix(native-chat): "Resume in chat" follows the terminal resume's host rule Agent Session History offered "Resume in chat" for a conversation recorded on this machine into a paired server's workspace, where its transcript does not exist. A chat now resumes a conversation only on the host that recorded it, as the terminal resume does, and that host is the one asked whether it can resume history. * fix(native-chat): the chat setting says older paired servers keep terminal chat * test(native-chat): pin that a host advertises the client-chosen launch mode * fix(native-chat): mirror this machine's chats only where it holds them Round 1 ran the local chat mirror for everyone so existing chats show whatever the setting says. That gave every desktop a permanent session-tabs listener, which turns on the runtime's phone replication paths, plus two full session-tab censuses at startup, and made the browser client mirror its remote host a second time. The runtime now says whether it holds structured chats: its structured host is built only when saved chats were restored at startup or a client created one here, and it announces the moment one is built. The mirror, the startup restore and the continue-after-restart offer run only when the setting launches chats or the host holds some, and never in the browser client. A chat a paired client creates here with the setting off still appears at once. The chat behaviour settings show wherever chats exist, and the setting's copy says it picks what new agents open as. The toggle-off teardown this made dead is removed. * test(native-chat): route a paired-server launch over the capability lists both sides really advertise * test(native-chat): record install listeners without a cast * fix(native-chat): a paired server admits a chat before any of it exists here The desktop opened a paired server's chat tab, launch record, queued prompt and focus intent before asking the server, so a "no" needed a replacement that undid and redid all of it, and every piece it missed was a bug: the workspace deselected, the caller told "failed" while a terminal ran its prompt, the caller's arguments and other queued prompts lost, and a create whose reply was lost treated as never sent. A paired launch now asks the server first and commits nothing until it answers. Admitted opens the chat as before. Declined runs the caller's own launch as the server's terminal, with the existing notice (a resume fails instead, having no terminal equivalent). Unreachable opens nothing and names the server in one toast. The new-tab launcher reports the host's surface for paired workspaces, as it did before paired chats, with the prompt delivery of whichever surface got the prompt. The replacement and its error classes are gone, and the probe inside a launch is back to its old meaning: a "no" is a failed chat with Retry, and no answer leaves "Could not confirm" with Retry and the prompt kept, here as on this machine. * fix(native-chat): mirror this machine's chats only once it holds one, not once its host is built Session history, resume preparation, terminal resume commands and replay-safe phone launches all build the structured host for users who never had a chat, which turned on the chat mirror and the structured-only settings rows until the next restart. The signal is now derived from the host's records (or a records file still owed its import) and pushed when the first chat is restored or created. A throwing listener no longer fails the install that fired it. * fix(native-chat): a fork's reveal never seeds a terminal beside the surface the launcher opens Forking into a paired-server workspace revealed it as if nothing would open there, so the reveal created a blank host terminal beside the forked chat (and beside a forked agent terminal on main). The launcher always opens the fork's surface itself, so the reveal now says so for every surface, as the fix-checks launch already does. * fix(native-chat): a declined direct launch keeps the caller's CLI args; an unreachable resume toasts once When a paired server declines a "Fix checks" chat in a new workspace, the terminal that opens instead now carries the recipe's saved CLI arguments, launch platform and launch source, as the terminal route did. "Resume in chat" to a server that cannot be reached showed the admission's "Could not reach" toast and the vault's generic one; the admission marks its failure notified and the vault adds nothing. * fix(native-chat): a declined background create opens its terminal without switching workspaces Since #23974 a worktree create the user moved away from must not pull them onto the new workspace. When a paired server declined that create's chat, the fallback terminal opened as a new agent tab, whose host create selects the workspace. The create now opens its own agent terminal the way main's background branch does: in place from the request's startup plan (so its CLI args carry), without selecting the workspace. A create the user is still watching keeps the new-tab fallback. * test(native-chat): name the launch's host in main's new outbox fence test Main's new staging-failure test calls settleStructuredAgentLaunchPrompt without the target this PR made required; it is a local launch, as in the sibling tests. * fix(native-chat): a paired server's new chat shows no model until the server reports the one it started A chat on a paired server starts with the server's saved model and options, but the picker showed this desktop's saved selection (or the catalog default) until the server reported a model, and a pick made in that window was remembered on the server under that guessed model. A paired launch now carries no desktop seed, and until the server reports its model the picker names no model and takes no picks. Local chats are unchanged. * test(native-chat): seed the paired repo without a cast The repo literal already satisfies Repo, so the changed-lines cast gate has nothing to excuse. * feat(native-chat): createSupport reports the saved selection a new chat on this host starts with A chat on a paired server starts with the server's saved model and options, which the desktop could not read, so its picker showed a guess. createSupport's answer, which the desktop already waits for before a paired launch, now also carries that seed as a new optional field (older clients ignore it). Create and createSupport read it through one resolver so they cannot drift. * fix(native-chat): a paired server's new chat shows the selection the server will start it with The paired server now names its saved model and options in the admission answer the desktop already waits for. That seed goes into the launch intent and its persisted record, so the picker shows the server's model at once, stays pickable like a local chat, and remembers picks on the server under that model; a reload shows the same. The locked picker remains only for a server too old to name a seed. Also moves host admission and launch-outcome tracking into their own modules: the latest main merge left structured-agent-session-launch.ts over the max-lines limit. * test(native-chat): expect the launch intent's new seed argument in exact-call assertions * refactor(protocol): move the Electron remote client capability list into its own module Merging main left protocol-version.ts one line over the max-lines limit on this branch. The list of capabilities the desktop advertises to a paired host moves, unchanged, into electron-remote-runtime-client-capabilities.ts, the module the next PR in the stack already uses for it; importers point there. * fix(native-chat): a paired chat with no saved server model is pickable; Retry shows the server's current seed A server whose user never saved a chat model sends no seed, and the desktop showed a locked, model-only picker for it, although that is the common case: no server that can admit a paired chat predates the seed field. Such a chat now behaves like a local chat with no saved model: the CLI default, pickable. The lock and its snapshot helper are gone. Retry kept the first admission's seed while the create probe, which already runs on every attempt, reported the server's current one and dropped it. The probe's seed now replaces a paired launch's seed and the picker's, so a retried chat shows what its create will run. * test(cross-version): stub the launch seed resolver createSupport now reads * test(protocol): pin the desktop capability divergence against what a paired server receives Every paired transport sends the shared remote base plus the Electron list, so the divergence test now compares that union with the renderer's local list instead of the declared Electron list. A capability added only to the shared base can no longer slip past it. The two base-only capabilities it surfaced are recorded: skills.install-result.v2 has no local caller; the authoritative-inventory label is read by the local tabs sync but dropped by main, and is marked unsettled. The turn-item and both background-task-stop capabilities were already sent through the shared base, so the Electron list no longer repeats them. The wire set is unchanged; this PR's real change on the wire is structured.v1, the Claude structured capability and the client launch-mode capability. * fix(native-chat): the desktop tells its own host it picks each launch mode, so retrying an existing chat works with the setting off * docs(native-chat): name the real exit for the released-phone createSupport rule * fix(native-chat): the route reads the capabilities a paired host actually receives The renderer decided whether a paired host would admit a chat from the desktop's Electron list, but every desktop transport sends that list plus the shared remote base. Th…
* feat(i18n): localize onboarding checklist steps to Japanese
Add Japanese translations for the 16 setup-checklist step names and
descriptions (`auto.components.feature.wall.feature.wall.setup.checklist.
localized.copy.*`) that were still falling back to English in the Setup
Guide modal and settings pane, and extend the checklist copy test so the
Japanese catalog is guarded alongside Korean.
* fix(i18n): render Multi-task as 並列開発 in the Japanese setup checklist
並列 is the term Claude Code's Japanese docs ("並列セッション"), Cursor's
Japanese coverage ("並列エージェント"), and the Zenn/Qiita worktree
articles use for running several worktrees or agents at once; 並行 was
the outlier.
* test(i18n): assert setup checklist catalog values are strings
* fix(i18n): align the Setup Guide subtitle with 並列 in Japanese
The subtitle sits directly above the 並列開発 step in the same modal and
the Korean catalog already renders "parallel agent work" as 병렬; use
並列 in Japanese too so the modal does not mix 並行 and 並列.
* fix(i18n): reconcile Japanese checklist copy with current catalog
---------
Co-authored-by: Neil <neil@stably.ai>
…stopped (stablyai#25056) * fix(native-chat): a Stop binds only the turn it actually stopped A Stop pressed before any turn showed used to claim, at end-write time, whatever turn the stopped send later opened, even when the Stop stopped nothing. A turn that then died on its own read as "Interrupted" (your cancellation) instead of "Failed". Now a person's Stop that named no turn binds, in memory only, every turn that ends while the Stop settles, and afterwards only the turn its interrupt took. The settle ends a still-running stopped turn once. A relaunch finds nothing in memory, so an unsettled turnless Stop binds no turn. A Codex Stop whose answered turn does not open within its wait, or whose send's answer was lost, now answers refused, so the host ends the child and the turn can never run. * fix(native-chat): keep the person's queue pause and close binding after a Stop settles A host stop or eviction with no turn running now defers to a person's Stop while its queue pause still holds with nothing sent since, read from rows, so a held card is not handed off on reopen after a Stop that did nothing or whose kill failed. A person's close that named no turn opens a settle around its child's end, so a turn that end cuts reads as theirs. A press opens its settle only when the latest Stop event is its own or the one in force it repeats: a late Stop, a card's interrupt or a lost event row reopens no earlier Stop. A Codex Stop that cannot reach a turn still able to open says the Stop is unconfirmed rather than that no turn ran, and a second Stop still reaches a turn an earlier wait left unopened. Also drops the unused openedBy plumbing and the unreachable "a written cancellation stays one" rule, and pins a relaunch after a named Stop. * fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle A Codex Stop that waited for a turn Codex answered a send into now answers "may still open" whenever that turn neither opened nor ended and its send is still owed, however the wait ended (it ran out, or the thread went idle). Before, a first press after an idle thread said no turn was running and kept Codex, while an identical second press ended it. Adds a test that a person's close the conversation outlives (as /clear does) closes its settle, so a later turn that ends on its own reads as a failure. * test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop With one ordered journal writer the Stop's event is in the fold when its write returns, so the press reads whether it owns the latest Stop from the fold instead of awaiting the write. Pins the case the read must refuse. * test(native-chat): name the settle, not a stream drain, in the Stop's own-end test * test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout Codex answers an interrupt it took before it sends turn/completed (interrupted): on TurnAborted the app-server answers pending interrupts, then ends the turn, on one channel. The test fake did the reverse. It now answers first and ends the turn on a later read, and the tests that read the turn's end right after a Stop wait for it. New end-to-end test through the shipped host, journal and Codex adapter: with the real order, every end row of the stopped turn reads interrupted by the Stop (named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the settle window turns the in-flight case red: the Stop's own end row then has no verdict, which reads as failed until Codex's end lands. * fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once A Stop that named no turn, made after Codex answered a send but before the turn opened, used to wait up to 5 s for the turn to open before interrupting, and ended the Codex process when it didn't. The stated reason, that Codex refuses an interrupt until it opens the turn, holds only part of the time: with no turn active, Codex takes an interrupt once its thread runs (turn_interrupt_inner), and refuses it with -32600 "no active turn to interrupt" before that or once the turn has ended. The Stop now sends the interrupt at once. Only on that refusal, while the turn has neither opened nor ended, does it wait for the turn to open (bounded at 5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One that never opens, or that an earlier wait already gave up on, fails the Stop, and the host ends the child as before. Sends still wait for the turn to open before steering into it. The test fake models Codex taking an interrupt once the thread runs (run()). * fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did A Stop whose interrupt Codex refused as finding no active turn skipped the wait when an earlier wait, a Stop's or a send's, had already given up on that turn. Every press now waits its own bound and retries once if the turn starts, so a turn that opens during a later press is still stopped. Both presses still reach the same verdict when it never starts. * fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send A Stop on a Codex turn that was answered but never opened ends the Codex process. That end settled the send as in doubt (unknown, recovered), and the client's outbox holds every later send behind a send in doubt until the person presses Retry, which re-sends the very message they stopped. The chat looked stuck. Codex records a prompt only once its turn has started, so a send whose turn never opened never ran. When the Stop's refusal says so (turnMayOpen), the child end now settles the unanswered sends as withdrawn, the verdict Codex's own interrupted-turn end already gives an unechoed send. The flag rides on the owed wind-down, so a retry after a failed child end withdraws them too. Claude's child end still leaves its unanswered send in doubt. * fix(native-chat): derive the withdrawal of a Codex send whose turn never opened Replaces the flag the Stop carried to the child's end, and its copy on the owed wind-down, with a reading of the journal at the settlement that lands. A Codex child's unanswered send is withdrawn when a person's Stop is in force since it was sent and no turn row ran, or was written, after it; any other end (a turn that opened, a host's close, a crash, Claude) still leaves it in doubt. A retried wind-down reads the same rows, so it withdraws the same sends. * fix(native-chat): keep a send in doubt when a turn was open for it The derived withdrawal read a turn as open for a send only if it still ran or was written after the send. A send steered into a running Codex turn whose interrupt failed met neither once the adapter's end settled that turn ahead of the host's settle, so it read withdrawn, though Codex drains a steer into the running turn and may hold it. A turn that ended after the send was handed over was open for it too: such a send stays in doubt, as before. Pins that case, and that a send made after the Stop, to a child that then dies before its turn opens, stays in doubt. * fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing Whether a turn was open for a send was read from end times: a turn that ended after the send's handover counted. A send made while a Stop ended the turn is handed over once that turn reads ended, yet Codex's own end for it can arrive later, so such a send whose own turn never opened read in doubt again, and the chat's queue held behind it. The handover already records where the send went: its message joins the turn running then (a steer) or belongs to no turn (it starts its own). Only a send that started its own turn, with none opened since, is withdrawn; one that joined a running turn, or has no recorded place, stays in doubt. The Codex test fake takes an answered interrupt as Codex does, dropping the turn before its end arrives. * fix(native-chat): leave no Stop row when the Stop took back a send that never ran A Stop on a Codex send whose turn never opened ends the child, and the child's end takes the send back into the composer. The Stop still wrote "Cancellation requested." at the conversation level, so with the send gone it sat under the previous finished turn and read as if that turn had been stopped. A Stop that found no turn running and whose child end took back every send it found now writes no row; a Stop of a running turn, or one that leaves a send in doubt, still does. * fix(native-chat): keep a Stop's note with the turn it waited for and stopped A Stop pressed while Codex was still opening the turn read the note's place before it waited: no turn was running yet, so "Cancellation requested." became a loose conversation row, never folded with the turn, drawn below whatever came next. Once the interrupt takes a turn that opened during that wait, the note is now scoped and keyed to that turn, as a normal Stop's is. A refused or unconfirmed Stop, and one that writes no row, are unchanged. * fix(native-chat): key the Stop's note by the turn the interrupt took * fix(native-chat): count a send whose answer was lost when a Stop takes it back The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart. * test(native-chat): pin which sends a Stop's child end can take back A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted. * test(native-chat): read the outbox reconcile from where main moved it
…pace row (stablyai#24904) * fix(native-chat): mark a chat whose start failed on its tab and workspace row A chat whose start the host refused said so only inside its own tab. Its row now carries the failed verdict the host publishes for a refused start, so the tab and the workspace row show it from elsewhere until a retry starts it or the tab closes. The + menu no longer shows a failed start as still starting, so launching the agent again retries it. * fix(native-chat): date the failed-start mark by the failure; keep the + menu change for a separate PR The failed-start mark on a chat's tab and workspace row is now dated by when the start failed, the way a failure the host reports is dated: it ages out on the same schedule, a restart does not bring an old mark back, and a failure the user already saw stays read. The failure time is saved with the launch record so a restart keeps it; records saved without it fall back to the tab's creation time. The + menu change (letting a failed agent be picked again) moves to its own PR: its correct form makes a new chat instead of retrying the failed one, which changes how launches join. * test: name the local host on the saved failed-launch record * fix(native-chat): re-check an unconfirmed chat start when its host is reachable again A chat create whose reply was lost, or that a reload interrupted, stayed unknown until the user pressed Retry in its tab. Each host subscription opens with one full inventory; on it, every launch that host owns whose outcome is unknown (including ones restored from the saved launch record) is re-checked through the existing Retry path, so it settles to started or failed. One still unknown waits for the host's next return; cancelled launches are skipped.
…e turn actually ends (stablyai#24369) * test(native-chat): a Stop over a card sent now into the running turn keeps it paused Red on main: Codex's turn end withdraws the steered hand-off and the queue sends the card again as a new host turn, with no pause recorded. * fix(native-chat): a Stop's queue pause holds a card whose hand-off is still unanswered A card sent now into the running turn was still pending when Stop judged the pause, so nothing was recorded; the interrupt then withdrew the hand-off, the card went back to waiting unpaused, and the queue sent it again as a new turn. The pause now counts a hand-off that may still return to waiting, judged with the appended row applied, so a withdrawal lands under the pause and an acceptance retires it in that same write. Codex and Claude both hit it. * test(native-chat): the Claude re-send case fails on its diff, inside the test's budget * test(native-chat): a pause held by an unanswered hand-off ends on every path that ends it The provider's answer, the provider dying, the chat closing, a restart, a withdrawal still owed at open, and a /clear (refused until the hand-off ends, then carrying every waiting card paused 'cleared'); each ends with the queue sending again. * fix(native-chat): narrow the pause's settled hand-off, and assert the queued receipt's card * refactor(native-chat): derive the queue's pause from Stop and Resume journal rows Stop now appends one journal row where it takes effect, before the interrupt, whatever the queue holds; Resume appends its own. The pause is a pure function of the fold: the latest Stop with no later Resume and no later accepted turn a person asked for. A /clear's carried cards name their source, which is the replacement's 'cleared' pause. Host-origin turns never lift either. One predicate decides which cards a pause holds; by default every waiting card without a hold of its own, including one queued after the Stop. The drain's consume re-judges it inside its own transaction. The rows are tombstones of an id no item takes, carrying the mark: a released host reads an unknown row kind as corruption and truncates the journal there. Deletes the stored pause (recordPause, the retire hook on every appended row, the settle-before-record step, mayReturnToWaiting and its row overlay) and the tests that only proved it retires. The queued_message_pauses table stays in the schema, unread and unwritten, for downgrade safety. * fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones A Stop's pause now holds only the cards queued before its row, plus a steer it withdrew, which returns to its own place. Each card records the journal position it was queued at, and the one hold rule compares that with the Stop row. A card queued after the Stop is a new instruction: it sends as usual, but the drain still stops at the first held card, so it never overtakes them. /clear's pause holds the cards it carried. Holding every card again is a one-line switch in that rule. * fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction The drain's pick and its consume now read one function, nextSendableQueuedCard, so a Stop row that lands between them holds a newer card behind an older held one exactly as the pick would. Notes why Stop and Resume ride a tombstone row. * fix(native-chat): stop creating the unused queue pause table The queue's pause is derived from journal rows, so nothing reads or writes queued_message_pauses. It was still created on every open "for downgrade safety", but an older build creates it itself when it opens the database, so the table only sat empty in every new database. The tests now pin that no pause table exists. * fix(native-chat): a Stop's pause never hides the restart pause A Stop holds only the cards queued before it. The pause derivation still returned the Stop alone whenever it was in force, so the restart pause was never considered: a card queued after the Stop, written by a host process that has since exited, sent by itself after Orca restarted, with no pause header and no Resume. A /clear pause that held nothing could hide it the same way. Every pause in force is now derived. A card is held if any of them holds it, and it names the first that does. The drain's pick, the consume transaction's re-check and the published header all read that one rule; the header names the pause holding the first card Resume would send. * test(native-chat): pin the Stop's no-resend, lift and held-card rules - The Claude and Codex Stop-withdraws-a-steer tests checked "not sent again" at one instant, before a queue ignoring the pause re-sends. They now wait for the stopped turn to end and re-check after a quiet window. - The deleted-card test read a card queued after the Stop, which sends whether or not a person's turn lifts it; it now reads the Stop's pause before and after that turn. - Unit cases pin that a Stop holds a card with no recorded position and one queued before a rewind. * refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller The Stop row that paused the queue becomes the general Stop event { reason, turnId?, at, caller? }, whose reason is the host's existing stop cause. It still rides a tombstone of a host-only id (a released host deletes the journal from the first unknown row kind), and Resume keeps its own marker on its own id. Only a person's Stop (reason user-stop) pauses the queue. * test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event * test(native-chat): pin that Stop and Resume rows never reach apps or count as history * test(native-chat): only a person's Stop event pauses the queue * test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop Through the real host: the event names the turn and who asked and is in the journal when the interrupt reaches the agent; at an agent still starting it is there before the start is ended and holds a card queued before it; an idle Stop writes one only when it withdrew a send; and the queue's claim re-judges a pause that landed after its pick. * test(native-chat): a card held at a starting agent is checked before the Stop's timing Also says precisely what the claim's in-transaction pause check defends against: the Stop and the drain share one serialized lane. * test(native-chat): a released build keeps and folds a journal holding Stop events Replays this build's rows from the released build's own journal database: every row is kept, the history after the Stop still folds, and an older client is sent only removed ids no item uses. * style(native-chat): format the Stop event changes * test(native-chat): type the released build's exports through one checked helper * fix(native-chat): the Stop/Resume row guard narrows to those tombstones only * test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade The Stop-event downgrade test ran in no CI lane: unit shards exclude the cross-version folder, and the cross-version lane runs a fixed file list that did not name it. It is now on that list. Its only case replayed the rows into a release's own fresh database, because that release cannot open the current host database. A second case opens the journal this build wrote with a main build that shares the database: it opens writable, keeps every row, appends, and this build then reopens it with the person's Stop still pausing the queue. * fix(native-chat): a Stop that stops nothing new writes no Stop event A Stop reaching a running agent wrote a Stop event on every press. Two presses before the first interrupt landed wrote two events, so a card queued between them counted as before the latest Stop and was held, though a card queued after a Stop should send normally. A Stop naming a turn that had already ended, as a phone sends late, also wrote an event for a turn it never stopped. It now writes one only when it withdrew a queued send, or stops something no event records yet: not a turn the journal no longer runs, and not the live turn a Stop still in force already names, unless a card was handed over into it since, which this Stop's interrupt sends back and must hold. The interrupt and the "already finished" note are unchanged. A Stop at a starting agent still always writes. * test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop * chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled * fix(native-chat): any later Stop event ends a person's Stop pause A person's Stop paused the queue until their next accepted turn or Resume, and a later Stop of another reason (the host stopping the agent, an eviction, a close) was ignored. Now the pause is the latest Stop event's: a later Stop of any reason ends a person's pause, and only a person's Stop pauses. The fold keeps the latest Stop event whatever its reason. An eviction of a resting chat writes no Stop event (a Stop that stops nothing writes nothing), so it cannot release held cards; a test pins that no event means no lift. * fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed A Stop pressed before the agent's turn shows in the journal (before Claude's echo, or before Codex opens the turn) records no turn. A second press once the turn showed compared that missing turn with the live one, wrote a second Stop event, and held a card queued between the presses. A repeat is now judged by what was sent since the Stop in force: with nothing sent after it (a refused send aside), a Stop that named no turn, or named the live one, is repeated and writes nothing. Anything sent since and not refused, including a send whose fate is unknown, makes the new press write, since its interrupt may send that card back to waiting. Tests: the two-press case across the turn showing; a steer between the presses settled unknown; and a Stop naming a turn that ended while the next card is sent but shows no turn yet, which writes and holds that card. The fold test that claimed an eviction path is renamed. * fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes A Stop or Resume row's value is read from disk with no shape check, and the pause fold stored whatever it found. A stored `stopEvent: null` would then throw on every pause check for that chat: the queue's pick, its send, and every queue update to clients. No build writes such a row, so this is hardening. The fold now reads a Stop only when it is an object with a string reason and a finite time, and a Resume only when it is `true`. Anything else is ignored: it pauses nothing and ends nothing. The row is still not treated as malformed, which could cut the history short. * fix(native-chat): a Stop still reads as yours after Orca restarts before the turn ends Every stop that ends work now writes the Stop's event before it ends the child: a person's close of the chat, an eviction (worktree teardown, orchestration stop, tab cleanup) and the idle sweep's stop of a start that never landed. A stop that ends nothing writes nothing, and quit writes none: its resume marker records why. The turn-end write reads the latest Stop event where every turn row is built, so the adapter's settle, the host's fallback and the relaunch's settle all agree: a turn a person's Stop or close named, ending with no verdict of its own after that Stop, ends as their cancellation. A relaunch's probe-bounded end is no earlier than a Stop that found the turn running. When the provider refuses the interrupt and the turn runs on, a refusal row answers the Stop, so a later crash still reads Failed; pressing Stop again after a refusal is a new Stop. * refactor(native-chat): a stop no longer carries its cause; the turn's end reads the Stop event The cause of a stop was threaded in memory from each entry through the host's stop step, the adapter router and each adapter's close onto the `ended` it settled with, and Claude kept a per-turn copy of a Stop it sent. All of that is gone: adapters settle a turn they cut as interrupted with no verdict, the host's fallback does the same, and the one rule where a turn row is built (`turnEndAfterStop`) reads the journal's latest Stop event to say whether it was a person's. - `closeSession` / `disposeSession` take no cause; `ended` has no `stopCause`. - Claude reads an error result after a person's Stop as their cancellation from the journal's Stop event (through the event sink), not from a per-turn slot, and a refused interrupt is the host's refusal row, not `withdrawTurnStop`. - An owed wind-down keeps no cause: its retry's fallback reads the Stop event. - The mutation context's Stop passes no cause: its step already wrote the event, and the delivery loop's child-end reason is read back from it. - A Stop pressed before its turn showed applies to the turn that opens under it, unless a send a person made since was accepted. * test(native-chat): a turn a later send opened is no Stop's that named no turn * test(native-chat): the restart test's death proof carries its detail * refactor(native-chat): a refused Stop leaves no record; a Stop only ever ends the turn it names The stop-refused mark is gone: its tombstone kind, its fold, the clock-keyed match that tied it to a Stop, and the exception that let a second press after a refusal write a new Stop. A Stop that stops nothing writes nothing. A Codex refusal names a turn that is no longer its active one, and the Stop names that turn, so the turn running instead never reads as the person's by its id alone. * fix(native-chat): a Stop pressed before any turn showed stops only the turn opened next A Stop that named no turn read as the person's cancellation for every later turn that opened after it, until a send a person made was accepted. The queue's drain, orchestration mail and a restart continuation send as the host, so a turn they opened long after, cut by a crash, read "Interrupted" as if the person had stopped it. The Stop now applies only to the first turn opened after it. * fix(native-chat): an older Claude's error end after a Stop pressed before its echo reads Interrupted Claude CLIs before 2.1.91 end an interrupted turn with an error result that names no reason. The translator judged whether a person's Stop explained it by its own copy of the Stop rule, which ignored a Stop that named no turn, so a Stop pressed before Claude echoed the send read "Failed". The translator now writes such an end as interrupted with no verdict and no error row whenever a person's Stop may name the turn, and the journal's one rule decides as it writes the end. * fix(native-chat): a person's Stop and /clear each name why they end the agent The host's mutation path ended the agent with one "recorded" ending for every caller, which read back the reason of whatever Stop event the journal held last, however old. /clear writes no Stop event, so its end took an unrelated earlier reason. Each caller now names its own: the chat's Stop `user-stop`, whose event its own step wrote, and /clear `user-close`, the user replacing this chat. * fix(native-chat): a host stop judges whether it ends work after the provider's rows land A close, eviction or host stop decided whether it ended a running turn from the journal as it stood, while the provider's own rows (the turn its echo opened) could still be in the session's event sink. A close landing in that gap wrote no Stop event, so the turn it cut read as news. It now reads after the sink drains, as a person's Stop does, through the same check; a drain that fails or takes over a second reads working. * fix(native-chat): a Claude Stop naming a turn that just ended still marks the follow-up it cuts A phone names the turn it last saw. When that turn had ended and a follow-up was still unechoed, Claude's Stop interrupted the follow-up and ended the child, but the Stop's event named the ended turn, so the follow-up's turn the child's end cut read "Failed" under "Cancellation requested.". A Stop that ends the provider's session ends whatever is in flight, so its event now names the live turn or none, and a Stop that names none binds the turn opened next. Codex keeps naming only the turn the Stop names. The Claude Stop turn-end tests move to their own file, since the session-ending Stop suite is at its line budget. * fix(native-chat): the idle sweep reads working by the same rule as a stop's event The sweep judged a chat resting while a send whose reply was lost was still unanswered, but the stop's event writer counts that send as work. So the sweep evicted it and wrote an evict event, which ends a person's Stop pause and let the cards behind it drain on their own. The sweep's owed work now reads the main agent working the way every session list and the event writer do. * test(native-chat): an aborted eviction's injected drain failure lands on the eviction's own drain A host stop now drains the session's sink once to judge whether it ends work, so the tests that fail the eviction's drain-published step skip that first drain. * fix(native-chat): the idle sweep's rest writes no Stop event; it evicts a send that never echoes The previous commit made the sweep count an unanswered send as owed work, which pins a chat whose admitted send Codex never echoes forever, and the sweep exists to retire exactly that. That rule returns. The sweep stops only an agent it judged resting, so its eviction now writes no Stop event, whatever send it retires: a person's Stop pause holds through it. * fix(native-chat): stopping a start that carries no send writes no Stop event A host stop, eviction or close of a starting child wrote a Stop event whatever the start carried. A start with a send already reads working, so the clause only mattered for a start with none, which ends no turn and no send: its event only lifted a person's Stop pause and bumped the idle clock, which is why the idle sweep had been changed to close the conversation in the same pass. The clause goes and the sweep is #24072's again. The child's end still reads host-stop, as before. * test(native-chat): a Stop's pause across a restart is tested with a restart that writes no event The rig's restart closes the chat with an eviction, which now writes a Stop event when work runs and so ends a person's Stop pause. "A Stop never hides a restart's pause" then passed with no Stop pause left to hide anything. Those tests, and the pause-lift test whose dropped assertion returns, restart as a process that dies with no close, which like a quit writes no Stop event, and assert that both the Stop's and the restart's pauses are in force first. * fix(native-chat): a host stop of a turn a person's Stop is still ending keeps that Stop's reason An eviction or host stop that landed while a person's Stop or close was already ending the same turn wrote a newer Stop event, and the turn's end reads only the latest, so the person's Stop of that turn read as news. A host reason now writes nothing while a person's Stop still decides what runs: the live turn it names or bound, or, with none, the turn a send opens next. The person's own close still writes. The E2 tests now open and end the stopped send's own turn, as Codex does, so the mail turn after it is not the turnless Stop's. * fix(native-chat): an older Claude's error on a later turn keeps its error text after a Stop The translator left an error result that names no reason to the journal's Stop rule whenever a person's Stop named the turn or none, but the rule binds a Stop naming no turn only to the turn opened next. So a real error on a later turn read "Failed" with its error text dropped. The translator now asks the journal's rule itself (`personStopDecidesTurn`, the one core `turnEndAfterStop` and a host stop's in-force check share), so the two cannot disagree. * fix(native-chat): a Stop of a start that never landed binds no later turn, whatever sent it A person's Stop pressed while the agent starts names no turn, and the send it stopped is cancelled before it opens one. The Stop then bound the next turn anything opened (orchestration mail, a restart continuation, the queue's drain, all of which send as the host), so a host eviction of that turn wrote nothing and its crash or close read as the person's cancellation. A Stop that named no turn now binds only a turn no send journaled after it opened: any send since, of any origin and not refused, opens its own. The E2 test's mail send is accepted as Codex accepts it, instead of opening the stopped send's own turn first. * test(native-chat): a rewind's restated turnless Stop binds no turn opened after the rewind A Codex rewind restates a person's Stop still in force after the turns it keeps, at a new sequence, so by sequence alone it would bind the next turn opened after the rewind. A send journaled after the restated row voids that binding (the previous commit), which this pins. * fix(native-chat): a relaunch settles a person's stopped turn with no "stopped while in progress" row After a restart, a turn a person's Stop ended reads "Interrupted after N" with the muted mark, but the relaunch still added the error row saying the provider stopped mid-response, which a live Stop never writes. The settle now skips that row when every turn it interrupts is the person's Stop's by the journal's one rule; a crash nobody stopped keeps it. * test(native-chat): the unexpected-exit settle's journal fake answers whether a person's Stop decides a turn * fix(native-chat): a host stop whose sink drain fails reads the journal as it stands A host stop drains the session's sink before judging whether it ends work, and a failed or slow drain read as working. So an eviction of an agent at rest wrote a Stop event that ended nothing, which lifts a person's Stop pause, and a close wrote a person's event naming no turn. The drain is now best effort: the stop goes ahead either way and only its record is at stake, so a failed or slow drain leaves the journal's read as it stands. A person's Stop keeps its own rule. * fix(native-chat): a Stop that named no turn applies only to a turn a send it stopped opened A person's Stop pressed before any turn showed names no turn. It bound the first turn opened after it, then (5ead1f6bccd) any turn opened by no later send, so a turn the host started for a card the Stop held, or for orchestration mail, read as the person's cancellation, and a host eviction of it wrote no Stop event when its send had been abandoned by the close first. The rule is now the concept itself: a Stop naming no turn applies to a turn opened by a send it stopped, one already handed to the agent at the Stop's position. Nothing new is stored. The turn's row names the send that opened it (Codex: the submission's key; Claude: the echo, which the journal aliases to the submission), and a handed-over send's item sits at its handover, so the target set is derived from the journal. A card the Stop held is handed over after it, so it is no target; a Stop of a start whose send never opens a turn binds nothing; a rewind keeps no submissions, so a restated Stop binds no turn opened after it. With no turn running, a host stop defers to the person's Stop only while every unanswered send is one it stopped. Claude's translator, which asks before its echo row lands, passes the send its echo acknowledged. * fix(native-chat): a host stop whose sink drain runs long reads the agent working; a failed one reads the journal A drain past its bound may still hold the turn's row, while the echo's acceptance has already landed, so the journal as it stands read nothing running: a person's close of that turn wrote no Stop event and the turn read as news. The two drain outcomes now differ: one that failed has nothing more to deliver, so the journal's read holds (as before); one still running reads working. * test(native-chat): a host stop with no turn running defers only while every unanswered send is the Stop's The branch had no test. An eviction with only the stopped send unanswered writes nothing; one with a send made after the Stop still unanswered writes its event. * fix(native-chat): a slow sink drain reads working only while an accepted send's turn row is due The previous commit read every drain past its bound as working, so a host eviction or stop of an agent at rest during a sink backlog wrote a Stop event that ended nothing and lifted a person's Stop pause. A slow drain now reads working only when the latest send the agent accepted has opened no turn the journal holds, the race it was for; otherwise the journal's read holds. * test(native-chat): the host-stop control keeps the stopped send unanswered beside the later one With both unanswered, the host writes only because not every unanswered send is the Stop's; a rule that deferred when any one was would pass the old control. * fix(native-chat): a steer is no send owed a turn when a slow drain judges a host stop A slow drain reads working when the latest accepted send has opened no turn yet. A Codex steer or a Claude fold is accepted into the running turn and never opens one, so a chat at rest whose last send was a steer still read working, and an eviction lifted a person's Stop pause. Sends delivered into a running turn, whose item carries that turn's scope, are skipped. * feat(native-chat): a chat reads Stopping from the person's Stop until the work it stopped ends The host derives it on each journal publish from the Stop's event, the live turn and the Stop's own answer, and publishes it as an optional field on the session status and the main agent's row. Clients present it: the chat's tail line and Stop control, the sidebar row, worktree ps and the phone's row. The chat and the phone also read their own Stop press until its request answers. * test(native-chat): pin Stopping on the phone and across mixed versions * test: give touched fake journals and mocks their SAFETY notes * test(native-chat): a turn waiting on the person reads attention, never Stopping * test(native-chat): the sidebar row follows Stopping when it is the only field that moved * test(mobile): the phone reads Stopping from its own Stop until the request answers * test(mobile): type the held Stop request instead of casting it * chore: keep the base lockfile (a local pnpm run rewrote it) * fix(native-chat): narrow the Stop note's optional failure; type the phone test's reply * test(native-chat): type the Stop test envelope's fields narrowly * fix(native-chat): a Stop the agent declined, or whose child end failed, says so while the turn runs on A Stop naming the turn that still runs, refused by the agent, now writes the Stop's refused fact instead of 'already finished'. A session-ending Stop whose child end fails while the work runs on revises its note to unconfirmed. * fix(native-chat): Stopping holds while any press of the Stop took A repeat press refused after an earlier press took no longer clears Stopping. Exit early when the Stop named a turn that is not the live one. * fix(native-chat): keep Stop enabled while the host says Stopping Only this client's own Stop request in flight disables Stop and Esc. A repeat Stop is how a stop the provider took but never answered escalates. * fix(sidebar): every agent row says Stopping in place of its tool line The dashboard row, which the sidebar's non-compact mode also draws, read the last tool line while a person's Stop ended the turn. It now shares the compact row's rule. * fix(mobile): the worktree list sees Stopping change on its own A snapshot whose only change was the host dropping Stopping compared equal and was thrown away, leaving the row on Stopping. * refactor(native-chat): fold the status feed in src/shared for both clients The snapshot merge and the contact-loss strip move out of the renderer feed so the phone folds the same stream the same way. * feat(mobile): let phones read the structured session status stream agentSession.subscribeStatus joins the mobile allowlist. The agent-session methods move to their own file, which the at-cap allowlist spreads in, and the allowlist test reads the Set instead of parsing the source. * feat(mobile): the phone chat reads Stopping from the host, like the desktop One status stream per client, opened on a host that advertises the status feed; a refusal to phones reads as no feed. The chat reads Stopping from the host or its own press, and holds Stop only while its own request is in flight. * test: give the new fakes checked types or a SAFETY reason * revert(native-chat): drop the refused-named-turn rewrite of a Stop's note Codex can send its refusal before the turn's end frames, so reading the turn as still live after a flush races; the Codex Stop that ends nothing is handled by ending the process instead. The base's 'already finished' note and its test expectation return. The Claude wind-down failure revise stays. * fix(mobile): release the status stream when the host ends it; refusals last one connection The feed now drops the handle of a stream the host ended or refused, so the logical client never replays it on a later session. A refusal to phones holds for one connection, so a host updated while the phone stays paired is asked again. * perf(native-chat): read the live turn's opener from its record when deriving Stopping After a Stop that named no turn, every later commit walked and copied the whole journal to find the live turn's record. The derivation now reads that record off the rendered snapshot's tail and decides with the same rule. * refactor(mobile): move the method-unavailable check into transport The status feed imported it from the Files tab's fallback. No behaviour change. * test(native-chat): a send after a Stop reads Working before its turn opens Pins the derivation's running-only read of the newest turn: the stopped turn, already ended, must not keep the next send on Stopping. * fix(sidebar): the compact row leads with Stopping so a narrow sidebar keeps it whole At the default width the row read 'Codex Chat - Stoppin…': the model and time keep their room and the line truncates from the end. Stopping now leads the line the way monitoring already does, so the chat name is what gets cut. Also pins that the turn bar keeps its running clock while the tail line says Stopping. * test(native-chat): the retry of a close whose exit was unproven writes no second Stop event The idle sweep finishes a stop left owed with that stop's own cause. It is the same stop, so its event stands alone and the child's end keeps the cause, for a person's close and an eviction. * fix(native-chat): read and write a Stop's answer by the turn its event records Stop notes are now one row per turn, keyed by the turn the Stop's event records. Performing a Stop and deriving Stopping share that key. A refused or unconfirmed answer never overwrites one that took at the same key; only a session-ending Stop's failed wind-down downgrades it, and that step now revises the note the Stop actually wrote, carried on the wind-down. Stopping reads the turn's note whenever it was first written, plus newer notes no other turn owns. Test fixtures gain the host logger and the phone's quietRepeatedStop. * refactor(native-chat): read a Stop's target once for its event and its note The chat's Stop now reads what it is aimed at (the named turn and whether the Stop ends the provider session) once, and both its event and its note's key derive their turn from that one value through the same rule. Adds the host test for a Stop naming an ended turn on a provider whose Stop ends the session. * fix(native-chat): the host never steers a message into a turn a Stop is ending A queued card's Send-now, or a send made while a person's Stop ends the turn, went to the agent as a steer into that turn. The delivery loop now holds any waiting message while the host's own Stopping reading holds, and sends it as its own turn once the turn ends. The Stopping reader also stops at the Stop's position and looks the turn's note up by key, instead of walking the whole journal. * feat(native-chat): while Stopping, the composer says a message runs after the stop Desktop and phone: the composer placeholder reads "Queue a message to run after the stop" while the chat reads Stopping, and a queued card's Steer (and the desktop's steer shortcut) is held. New key translated in all 6 catalogs. * refactor(native-chat): the chat pane's Stop controls live in their own module The pane went over its line limit once merged with main. Its Stopping reading, the press that holds Stop, and the steer and placeholder it hands the composer move to native-chat-structured-stop-controls.ts. * fix(native-chat): hold a send at its handover, reading the feed's own Stopping The hold was checked when the delivery step started, but the handover runs in a later step after waiting on the agent's start, so a Stop landing in between let a new send steer into the stopping turn. The check now runs at the handover. It reads the status feed's projection for the commit instead of rendering the journal again, so holding a send adds no journal read of its own. * refactor(native-chat): one display status decides Stopping on every surface agentStopDisplayStatus combines whether the agent works, the host's flag and this client's own press. The chat pane, sidebar and dashboard rows, and the phone's chat all read it, instead of each combining the flags. * fix(native-chat): Stopping holds until the stopped turn ends, whatever the Stop's answer A Stop the agent declined, or whose end went unconfirmed, used to drop the chat back to Working. It now stays on Stopping until the turn ends, and Stop stays enabled so a repeat press escalates. The Stop's answer is no longer read for Stopping, so its note goes back to the key the base gives it (the restore of queued-stop.ts and the removed key test landed in the previous commit). The note still keeps a press that took over a later refusal, and a failed process end still says the Stop went unconfirmed. * fix(native-chat): a Stop binds only the turn it actually stopped A Stop pressed before any turn showed used to claim, at end-write time, whatever turn the stopped send later opened, even when the Stop stopped nothing. A turn that then died on its own read as "Interrupted" (your cancellation) instead of "Failed". Now a person's Stop that named no turn binds, in memory only, every turn that ends while the Stop settles, and afterwards only the turn its interrupt took. The settle ends a still-running stopped turn once. A relaunch finds nothing in memory, so an unsettled turnless Stop binds no turn. A Codex Stop whose answered turn does not open within its wait, or whose send's answer was lost, now answers refused, so the host ends the child and the turn can never run. * fix(native-chat): keep the person's queue pause and close binding after a Stop settles A host stop or eviction with no turn running now defers to a person's Stop while its queue pause still holds with nothing sent since, read from rows, so a held card is not handed off on reopen after a Stop that did nothing or whose kill failed. A person's close that named no turn opens a settle around its child's end, so a turn that end cuts reads as theirs. A press opens its settle only when the latest Stop event is its own or the one in force it repeats: a late Stop, a card's interrupt or a lost event row reopens no earlier Stop. A Codex Stop that cannot reach a turn still able to open says the Stop is unconfirmed rather than that no turn ran, and a second Stop still reaches a turn an earlier wait left unopened. Also drops the unused openedBy plumbing and the unreachable "a written cancellation stays one" rule, and pins a relaunch after a named Stop. * fix(native-chat): keep a failed Stop's turn display-only, and settle edges off the commit path A Stop that failed marks the turn it could not stop for "Stopping…" only (JournalStopSettle.failedOn): no turn-end rule reads it, so that turn's own end with no verdict reads as a failure, not the person's. A settle edge writes no row, so it no longer goes through the journal's commit listener, which also delivers history, counts as activity for the idle sweep and schedules the queue drain. A narrow settle-edge hook republishes the status row and wakes the steer hold's handover, and nothing else. * fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle A Codex Stop that waited for a turn Codex answered a send into now answers "may still open" whenever that turn neither opened nor ended and its send is still owed, however the wait ended (it ran out, or the thread went idle). Before, a first press after an idle thread said no turn was running and kept Codex, while an identical second press ended it. Adds a test that a person's close the conversation outlives (as /clear does) closes its settle, so a later turn that ends on its own reads as a failure. * fix(native-chat): a Stop that failed before its turn showed still reads Stopping through that turn A Stop that failed with no turn open marked nothing, so the chat dropped to Working and the turn that then opened never read "Stopping…". The display-only mark now also covers that case: the first turn that opens after the Stop failed, provided no message was handed to the agent in between. No turn-end rule reads the mark, so that turn's own end with no verdict still reads as a failure. * test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop With one ordered journal writer the Stop's event is in the fold when its write returns, so the press reads whether it owns the latest Stop from the fold instead of awaiting the write. Pins the case the read must refuse. * test(native-chat): name the settle, not a stream drain, in the Stop's own-end test * test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout Codex answers an interrupt it took before it sends turn/completed (interrupted): on TurnAborted the app-server answers pending interrupts, then ends the turn, on one channel. The test fake did the reverse. It now answers first and ends the turn on a later read, and the tests that read the turn's end right after a Stop wait for it. New end-to-end test through the shipped host, journal and Codex adapter: with the real order, every end row of the stopped turn reads interrupted by the Stop (named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the settle window turns the in-flight case red: the Stop's own end row then has no verdict, which reads as failed until Codex's end lands. * test(native-chat): Stopping ends with a Codex turn whose interrupt is answered before its end With Codex's real order (the interrupt's answer, then turn/completed interrupted), the status shows Stopping while the Stop settles, drops it once the turn ends, and never carries a verdict other than the person's cancellation. * fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once A Stop that named no turn, made after Codex answered a send but before the turn opened, used to wait up to 5 s for the turn to open before interrupting, and ended the Codex process when it didn't. The stated reason, that Codex refuses an interrupt until it opens the turn, holds only part of the time: with no turn active, Codex takes an interrupt once its thread runs (turn_interrupt_inner), and refuses it with -32600 "no active turn to interrupt" before that or once the turn has ended. The Stop now sends the interrupt at once. Only on that refusal, while the turn has neither opened nor ended, does it wait for the turn to open (bounded at 5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One that never opens, or that an earlier wait already gave up on, fails the Stop, and the host ends the child as before. Sends still wait for the turn to open before steering into it. The test fake models Codex taking an interrupt once the thread runs (run()). * fix(mobile): name how the phone's status stream is released in the subscription inventory Main made each inventory entry state its release; the status feed's stream is released from its subscribe params, as the session event stream is. * fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did A Stop whose interrupt Codex refused as finding no active turn skipped the wait when an earlier wait, a Stop's or a send's, had already given up on that turn. Every press now waits its own bound and retries once if the turn starts, so a turn that opens during a later press is still stopped. Both presses still reach the same verdict when it never starts. * fix(codex): never steer a turn whose interrupt Codex answered Codex answers an interrupt as the turn aborts, before it sends that turn's turn/completed. In that gap the adapter still counted the turn as running, so a message handed over right after a Stop settled (the Stop's own end row already reads the turn ended) went out as turn/steer, which Codex refused with -32600 "no active turn to steer", and only then as turn/start. The adapter now marks a turn whose interrupt Codex answered as aborted until its turn/completed, never steers into it, and starts the message's own turn directly. Steering a turn that is genuinely running is unchanged. * fix(native-chat): a message sent while Stopping is queued as a card, whatever the setting While the chat reads Stopping (the host's flag or this client's own Stop in flight) there is no turn left to steer into, so the desktop asks the host to queue the send even with the queueing setting off, and it is never drawn as a bubble inside the turn being stopped. The phone already queued every send on a capable host; a test now pins that it does so while Stopping. * fix(native-chat): a message queued while Stopping is a card at once, not after the stop A person's Stop holds the session's lane until Codex answers its interrupt, and a send was admitted only behind it. By then the turn read ended, so a send that asked to be queued went out plain: no card for the whole of Stopping, then a bubble and a new turn. While the host reads that a person's Stop is ending the work, a text send that asks to be queued is admitted without waiting for the lane: the same ledger and lease admission, and a plan that only writes the card through the journal's ordered writer. The card runs when the stop lands; the Stop's pause holds only cards queued before it. Anything else, including a Stop that settled by the time the send runs, takes the lane as before. The Codex test fake now drops the active turn when it takes an interrupt, as Codex does before it answers, so a turn/start after the answer opens a new turn. * fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send A Stop on a Codex turn that was answered but never opened ends the Codex process. That end settled the send as in doubt (unknown, recovered), and the client's outbox holds every later send behind a send in doubt until the person presses Retry, which re-sends the very message they stopped. The chat looked stuck. Codex records a prompt only once its turn has started, so a send whose turn never opened never ran. When the Stop's refusal says so (turnMayOpen), the child end now settles the unanswered sends as withdrawn, the verdict Codex's own interrupted-turn end already gives an unechoed send. The flag rides on the owed wind-down, so a retry after a failed child end withdraws them too. Claude's child end still leaves its unanswered send in doubt. * fix(native-chat): derive the withdrawal of a Codex send whose turn never opened Replaces the flag the Stop carried to the child's end, and its copy on the owed wind-down, with a reading of the journal at the settlement that lands. A Codex child's unanswered send is withdrawn when a person's Stop is in force since it was sent and no turn row ran, or was written, after it; any other end (a turn that opened, a host's close, a crash, Claude) still leaves it in doubt. A retried wind-down reads the same rows, so it withdraws the same sends. * fix(native-chat): a card queued while Stopping runs past the cards the Stop holds A card queued before a person's Stop waits under its pause until Resume. One queued after it, as a message sent while Stopping now is, was stuck behind them too, since the queue never reorders. Such a card was asked for after the Stop, so it runs when the stop lands, past the cards held only by that Stop's pause; a returned card and the restart and /clear pauses still hold everything behind them. Also: the host's own Stopping reading is gated on working, as the published flag is, so a failed Stop's mark never reads Stopping on an idle session; a send that falls back to the lane re-reads the conversation's journal there; and the end-to-end test asserts the queue's pause rather than a per-card field. * fix(native-chat): a paused queue labels only the cards it holds Since a card queued after a person's Stop runs past the cards the Stop holds, labelling every card "paused" while the queue's pause is published misreads that card. The host now marks each card its pause holds (heldByPause, a new optional field), and the desktop and phone label only those. An older host marks none, so a client keeps today's labels; an older client ignores the field. Adds a test of the desktop's own send through the real outbox: while the chat reads Stopping, the request asks the host to queue it and no bubble is drawn, on a host that advertises the queue. * revert(native-chat): defer the per-card queue pause label to the queue's rollout The heldByPause field and its labels are visible only where the host advertises the queued-messages capability, which shipped hosts do not yet do. Deferred to that rollout; the real-outbox send test stays. * fix(native-chat): while Stopping, say and show what a send does where the queue is dark Shipped hosts do not advertise the queued-messages capability, so a message sent while Stopping goes out plain: the host holds it until the stopped turn ends and then runs it as its own turn. The composer still said "Queue a message to run after the stop", and the message was drawn inside the turn being stopped. Now the placeholder reads "Send a message to run after the stop" where the host does not queue sends, and "Queue a message…" only where it does (desktop and phone, all six catalogs). A send this client made that the host has not recorded yet is drawn after the Stopping line while the chat reads Stopping, as a message held behind a running command already is; once the host hands it over it opens its own turn. Client presentation only. * fix(native-chat): keep a send in doubt when a turn was open for it The derived withdrawal read a turn as open for a send only if it still ran or was written after the send. A send steered into a running Codex turn whose interrupt failed met neither once the adapter's end settled that turn ahead of the host's settle, so it read withdrawn, though Codex drains a steer into the running turn and may hold it. A turn that ended after the send was handed over was open for it too: such a send stays in doubt, as before. Pins that case, and that a send made after the Stop, to a child that then dies before its turn opens, stays in doubt. * fix(native-chat): restore the per-card queue pause label Kept after all: a paused queue labels only the cards it holds (heldByPause), which is visible only where the host advertises the queued-messages capability. * fix(native-chat): draw only a send made while Stopping after the Stopping line Every send the host had not recorded yet was drawn after the Stopping line, including one made just before the Stop, which the host steers into the turn; it then jumped up into that turn once recorded. The outbox now marks a send made while the chat reads Stopping, and only those wait after the line. * fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing Whether a turn was open for a send was read from end times: a turn that ended after the send's handover counted. A send made while a Stop ended the turn is handed over once that turn reads ended, yet Codex's own end for it can arrive later, so such a send whose own turn never opened read in doubt again, and the chat's queue held behind it. The handover already records where the send went: its message joins the turn running then (a steer) or belongs to no turn (it starts its own). Only a send that started its own turn, with none opened since, is withdrawn; one that joined a running turn, or has no recorded place, stays in doubt. The Codex test fake takes an answered interrupt as Codex does, dropping the turn before its end arrives. * refactor(native-chat): move queue-while-stopping to its own follow-up The queued-messages capability is off on every shipped host (#21062), so the parts of this PR that act only when it is on move to a follow-up stacked on this one: admitting a queued card while a Stop holds the session's lane, a card queued after a Stop running past the cards it holds, the per-card pause mark, and asking the host to queue a send made while Stopping. This PR keeps the Stopping state, the host's steer hold, the rule that never steers a turn whose interrupt Codex answered, and what a send while Stopping looks like where the queue is off. * fix(native-chat): leave no Stop row when the Stop took back a send that never ran A Stop on a Codex send whose turn never opened ends the child, and the child's end takes the send back into the composer. The Stop still wrote "Cancellation requested." at the conversation level, so with the send gone it sat under the previous finished turn and read as if that turn had been stopped. A Stop that found no turn running and whose child end took back every send it found now writes no row; a Stop of a running turn, or one that leaves a send in doubt, still does. * fix(native-chat): count a send whose answer was lost when a Stop takes it back The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart. * test(native-chat): pin which sends a Stop's child end can take back A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted. * fix(native-chat): read the host's Stopping beside main's startup phase Main now reads only the startup phase from the status feed and no longer publishes which child is starting. The chat reads the host's Stopping from its own hook beside it, and the Stopping bridge test mocks the execution-host lookup main's owner resolution now calls. * test(native-chat): read the outbox reconcile from where main moved it * fix(native-chat): a retried message no longer waits behind a later Stop Retry dropped the Stop it had outlived but kept the mark that it was sent while a Stop was ending a turn, so a retried message waited behind whatever later, unrelated turn a Stop was ending. Retry is a new send: drop that mark too. * fix(native-chat): word a send after a Stop by whether this send will queue The 'queue a message to run after the stop' placeholder read the host's queue capability alone. A send queues only when the host queues and this send asks it to: the queue setting is on and no pending prompt blocks the queue. Desktop and phone now word the placeholder from that same decision their send uses. * test(native-chat): one test per case for the words of a send after a Stop * refactor(native-chat): the dictation hook owns the composer's dictation state Keeps NativeChatComposer within its line limit after the Stop props and main's /context answer both landed in it. * refactor(native-chat): name the dictation hook for what it owns now * fix(native-chat): a Stop's note says it took once a joined close proves the exit A session-ending Stop whose child's end failed revises its note to 'unconfirmed'. Since main's #24862, the next Stop joins that close rather than stopping again, and wrote no note, so a close that then proved the exit left 'unconfirmed' under a turn that ended. The close now carries the note it settles, and its proven exit revises it to 'Cancellation requested.'. A join that fails again leaves it unconfirmed. * fix(native-chat): a proven turn end says a Stop's unconfirmed note took Replaces the note carried on the child's close. Every settlement that ends turns interrupted on a proven exit, live or after a crash, also revises an unconfirmed Stop note on those turns to 'Cancellation requested.', found by the note's turn scope, in the same batch. A note a Stop wrote before its turn showed is re-keyed onto the running turn when it becomes unconfirmed, in one batch, so that end finds it. Known limit: with no turn open yet, the note keeps its key and no turn's end revises it. * test(native-chat): a Stop's unconfirmed note says it took when the agent exits on its own * test(native-chat): name the Codex handle as main's opaque handle does * test: restore the provider handle import the main merge dropped * fix: derive Stop note wording from interrupted turns * test: name the raw replay case for what it covers * refactor(native-chat): move the waiting-slot split into its own hook * test: follow main's chat font-size rename in the stopping tests * test: follow main's single live-line value in the Stopping tests * test: give the android live-line fixtures the stopping field * chore: keep the session host under its line limit after the main merge * chore: keep the composer test and the phone chat view under their line limits after the main merge The Stop control now disables itself while Stopping, so the composer passes the flag through and its test file stays as main has it. The phone chat header's Stop moves to its own component. * perf(native-chat): read a Stop note's fields before parsing its key on every snapshot Every snapshot projects each item through the Stop-note read, and each new snapshot rebuilds the index of Stop notes by turn. Both parsed every item's key first; they now check the row's kind and turn scope (and, for the projection, its unconfirmed-stop failure) before the parse. Every Stop note is a status row, so what each finds is unchanged.
…tablyai#24369 (stablyai#25949) A test still called isStructuredAgentSessionThinking with a bare array after stablyai#25751 changed it to take { items }, and stablyai#24369 left the session host one line over its max-lines budget. Both fail CI on every PR.
…ablyai#23956) * feat(native-chat): attach several files from one picker selection Picks over 256 files are rejected whole, matching the drop limit. Fixes stablyai#23905 * refactor(native-chat): name the composer action for the files it now attaches The hook still returned `pickAttachment` after it started attaching a whole selection. Also records why the plural IPC handler sits next to the singular one, matching its siblings in the same file. * test(native-chat): cover the paperclip forwarding every picked path The one line that makes the feature work — the composer handing the whole picker result to attachExternalPaths — had no test; reverting it to paths[0] kept every existing test green. Adds a hook test for the multi-path, canceled, and wrong-pane cases, plus the missing canceled case for the shell:pickAttachments handler. * test(native-chat): exercise multi-pick through renderHook * test(settings): preserve label fallback and verify Spanish copy Keep latest main's missing-label fixture and restore its translation namespace after cleanup, so later tests still load real catalogs. Independently assert the shipped Spanish resource and rendered supported-agents label. --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…urfaces (stablyai#24595) * fix(native-chat): say each chat outcome once and drop internal-only surfaces After a restart, resuming a chat reported the same outcome in up to four places: a toast, the chat's own note, the status bar entry and the resume dialog. Success also toasted, and a lost answer or unconfirmed dismissal raised toasts of their own. The chat's note and the status bar entry (which opens the dialog listing every affected chat) now carry it alone, and the resume guidance no longer talks about which process "owns this session". Provider events Orca does not recognise and that carry no words of their own are logged instead of journaled as raw "codex · notification:foo" rows. Rows with the provider's own sentence, failures, and records of replies Orca already sent stay. The approval card no longer shows the matched ask rule, its settings source or a "Blocked path" line; it keeps the request, its detail and the reason. * fix(native-chat): store wordless provider events, hide them only when drawn Dropping text-less provider events on the host also dropped Codex's turn/plan/updated row, which the task list (checklist and composer progress panel) is built from. The host now journals these rows exactly as before, and desktop and mobile chats leave out a row whose only text is the host's "provider · kind" label. Failures, notices, rows with the provider's own sentence and records of requests Orca answered still show. A hidden legacy Codex collab-call row still counts as the agent acting for which subagent section stays open. Claude's local slash command output is read from `content` for that one event kind only, so it shows as the row's text instead of disappearing. * fix(native-chat): rows no client draws never fill the Codex cap or split a turn's work - Codex per-turn cap: a wordless provider row (decided by the same shared rule the clients use) has its own silent storage bound, so it no longer crowds out rows that draw and never produces a "N more provider notifications not shown" line about rows nobody sees. Plan updates are exempt from the cap, so the task list keeps updating after a noisy turn. - A Claude local command's row an older host stored with only its label (/usage output in the payload) stays drawn instead of disappearing. - A settled turn whose only extra row draws nothing offers no expand arrow, and a wordless row no longer splits one tool run into two. * fix(native-chat): a resume request that never reached the chats says so once, with Retry When the resume request fails before the host reserved anything (the host is not ready, or its recovery record cannot be written), the host has nothing to record, and the chats used to fall silently back to "N chats to resume". The status bar and dialog now show them as failed, with the existing guidance and Retry, until Retry, Dismiss, a host answer that no longer offers them, or a relaunch. Comments that still cited the removed toast are corrected. * fix(native-chat): keep mobile tool runs whole and a failed Dismiss's chat marked - Mobile draws no task list, so it now drops wordless provider rows, plan updates included, before folding tool runs; a mid-run plan update no longer leaves two run headers back to back. - A Dismiss the host never took keeps a lost resume request's chat shown as failed instead of turning it back into a plain offer. The lost request's real error now goes to the renderer log, since the row carries this side's own reason code. - Comments corrected: the Codex cap no longer claims dropped wordless rows stay countable, and the resume comments cover a chat a lost request never reached. * fix(native-chat): tell the user what their Resume click did, in one toast Clicking Resume in the dialog (or Retry on a row) closes the dialog and acts on chats that are mostly off-screen, and nothing said what happened. One toast now summarises that click: "Resumed N chats and asked them to continue", "N chats couldn't be resumed" with Show (opens the dialog), "Couldn't confirm N chats were resumed", or the mixed case in the same toast. A request lost before it reached the chats counts its chats as not resumed. An opted-in automatic resume at launch raises no toast; dismissal and other bookkeeping stay silent. * fix(native-chat): Show on the Resume toast re-reads before opening; fix toast translations Show could open nothing when the host retired the failure while the toast was up, because the dialog draws nothing without rows and the request stayed latched. Show now goes through the status bar entry's re-read-then-open path, moved into the store so both use it. The French, Japanese, Korean and Chinese "Resumed N chats and asked them to continue" strings are rewritten to match their sibling strings. Tests pin Show on the toast of a lost resume request whose chats the re-read still lists. * test(native-chat): click the Resume toast's Show through a typed helper The tests reached sonner's action handler with Reflect.apply, which the anti-slop lint rejects. One test-support helper now returns the last toast's Show as a typed click. * fix(native-chat): name the path an approval needs when the request doesn't show it The approval card dropped Claude's blocked path along with the ask rule, so a request like `git push` that needs access to `.git/config` no longer said what it touches. Bring the path back as "Needs access to: <path>", only when the description, reason or request detail doesn't already show it (verbatim or JSON-escaped). One shared rule serves desktop and mobile; the ask rule stays out. * fix(native-chat): count a blocked path as shown only when the text names the whole path '/srv/data' inside '/srv/data-old/x' hid the "Needs access to" line although the path itself was never shown. A match now needs the next character to end the path: end of text, whitespace, a quote or a separator. Also pin that the desktop approval mapping passes the blocked path through and drops the ask rule. * fix(native-chat): the automatic resume at launch gets the same one outcome toast as a click An opted-in launch resume now calls the same announce as a clicked Resume, with the same copy: how many resumed, how many couldn't (with Show), unconfirmed, or mixed, said once. Dismiss and bookkeeping stay silent; a launch whose chats all moved on raises nothing. * fix(native-chat): the resume success toast says "Resumed N chats" Each chat's own note already says Orca asked it to continue, so the toast drops "and asked it/them to continue" in every locale; the couldn't-resume and couldn't-confirm counts and Show are unchanged.
…st comes back after an outage (stablyai#25827) * test(remote-browser): reproduce a client-hosted browser tab left unavailable after a network outage A paired desktop that loses its host for longer than the browser-host lease grace (25s reset cut, or a 100s silent cut the host's heartbeat reaps) tears down its client-hosted pages and never hosts them again, leaving 'Client-hosted browser unavailable'. Red on main. Extends the freezable TCP proxy with a full silent or reset link cut. * fix(remote-browser): host client browser pages again when a paired host comes back after an outage When the network between a paired desktop and its Orca host dropped for longer than the browser-host lease grace, the desktop tore down its client-hosted pages and never offered to host them again, so the tab stayed on 'Client-hosted browser unavailable' until the app was relaunched. Regaining contact with a runtime now re-claims browser hosting the same way a runtime restart already did: if this desktop still has client-placed pages for that host, it starts its browser client host again and the host's existing recovery re-places them, reloading at their last URL. The notice now says the host is offline (and that the page reloads when it reconnects), names another desktop only when the placement belongs to one, and otherwise says the page is not available here right now. * fix(remote-browser): claim offline only for a host that can reconnect, and re-host from the notice The unavailable notice said the host was offline for any null status, which also covers a retired or refused pairing and a host whose runtime did not answer. It now uses the host connection state and excludes revoked verdicts. When the control link returns while the old browser host is still inside its lease reconnect, the contact-edge ensure is a no-op; if that reconnect then fails, nothing re-hosts. The notice now re-claims hosting on mount while the host is reachable, which is exactly that stranded state. Plain regained contact no longer queues a second preparation behind one already in flight; only a runtime restart does. * fix(remote-browser): re-prepare browser hosting on every regained contact A preparation still in flight when contact returns started while the host was unreachable, so it is the one most likely to fail. Coalescing the contact edge into it dropped the only trigger that could succeed and left the page unavailable. Always queue one follow-up preparation instead.
…blyai#25944) * fix(sidebar): scope lane collapse and reveal by execution host * fix: consolidate sidebar reveal group key imports * test: publish cancellation readiness after signal setup
…ed (stablyai#25743) * fix(codex): write Orca's hook in ~/.codex only when something changed One reconcile replaces the per-launch writer and its background approval session. It runs at app start after PATH hydration, when the setting turns on, once per native pane spawn, and (with a bounded 3 s wait) on Codex launches and resumes. Orca's entry lives alone in a matcherless group, appended last unless already in place; other copies are removed and the shifted user approvals move verbatim under both key spellings. The approval, with Codex's own hash, goes in first. The routing gate closes only for a hooks.json with a bad shape. * fix(codex): report hook status for the home the next pane uses Status reads ~/.codex (both key spellings) when launches use it, or the CLI has no selection, and the selected managed home otherwise. It explains: update Codex, Codex not found, not asked yet, approved by Orca but not yet confirmed by Codex, a hooks.json shape that moves panes to Orca's own home, and inline config.toml approvals Orca cannot add to. * test(codex): pin the ~/.codex approval against a real Codex, and run it on those files The contract now also writes Orca's entry into a throwaway ~/.codex and checks that Codex lists it trusted and enabled, turns it back on over a /hooks switch-off, lists it for review after a user inserts a hook ahead until the next check, keeps an inline config.toml loadable, and shows no review in a real TUI start. The real-binary CI job now runs when the ~/.codex writer changes. * test(ci): keep the scope test under its line cap; assert the ~/.codex paths beside the contract job * test(codex): type the hoisted test holders instead of asserting * test(codex): cover the matcher rule, an older build's event, and the spawn trigger Adds tests that failed against mutants which survived the first pass: an entry alone in a matcher group, a current copy's approval in an event left to an older build, one run per spawn, a spawn riding a running reconcile, and the native-only spawn trigger. The opt-out's Codex-hash cleanup now runs once, after the sweep, instead of twice. * docs(codex): name the ~/.codex reconcile in the legacy sweep's lane comment * fix(codex): approve ~/.codex with Orca's own hash while Codex's answer is pending The reconcile waits at most 0.5 s for Codex's hash. If the lookup is still running, an entry already in place keeps its approval and nothing is written; a missing entry or approval gets Orca's computed hash, approval first, inside a launch's 3 s wait, as main's did. When the lookup lands, the reconcile runs again and Codex's hash replaces it. * refactor(codex): one start for the hook lookup and the ~/.codex reconcile startCodexHooks replaces the two start functions and takes the PATH wait that the reconcile request's after option carried. A spawn reconciles unless one is running, without a scheduled flag, launches call one reconcileCodexHooksForLaunch on the shared withTimeout, and the reconcile alone reads the hooks setting. The startup test now runs the ready phase instead of matching its source text. * refactor(codex): plan ~/.codex with the shared Orca-hook pruner and approval reader The planner prunes with removeManagedCommands, as the opt-out does (so a hook that runs Orca's script through its args goes too), and returns a prune or settle union. While Codex has not answered, the kept approval comes from the shared per-event reader. The reconcile result is its outcome alone, a concurrent save spends a pass of the one bound, the opt-out removes Orca's approvals once, the approval-first writer takes the hooks path, and getRealHomeConfigTomlPath gives way to getSystemCodexConfigTomlPath. * fix(codex): ~/.codex keeps only an approval holding a hash Orca's entry may carry * refactor(codex): name the ~/.codex hooks-file check for what it reports * test(codex): the ~/.codex entry tests know Codex's earlier hashes, as the app's lookup does * refactor(codex): the ~/.codex reconcile uses the lookup's answer names * test(codex): ~/.codex status tests keep a Codex on PATH unless one is missing * refactor(codex): one stopgap for both homes; the ~/.codex pass finds its own home, hash and user data Also passes every approval to the approval-first writer, which already skips the ones in place. * refactor(codex): the reconcile start owns the warm-up; no rerun-on-answer flag The lookup start only records the hydrated PATH; one catch, one request type, and the app config is kept as given. * refactor(codex): the ~/.codex pass returns nothing; tests read the files Also makes the ~/.codex opt-out take Codex's hashes, as every production caller passes them. * refactor(codex): the ~/.codex approval cleanup takes Codex's hashes * test(startup): check the Codex hook start waits for PATH by behavior, not identity * fix(codex): a status-hook problem never moves the system default off ~/.codex * chore(ci): run the real-Codex contract when the ~/.codex reconcile changes * docs(codex): the ~/.codex reconcile's refused branch covers every definitive refusal * test(codex): start the failure-memo lookup with the reconcile's PATH-only start * fix(codex): ~/.codex gets nothing while Codex is missing, a pending answer uses the saved one, a conversion waits for a run that writes, and status and the reconcile read one home and one Orca-hash rule * test(codex): ~/.codex and managed-home status tests name their home; ~/.codex rewrites the backslash key Codex on Windows reads * test(codex): the Windows upgrade test reads status for the managed home it installs * test(codex): the real-TUI contract trusts its workdir by its real path, and its userData exists * fix(codex): keep Orca's approval at every slot its entry holds in ~/.codex
…yai#23770) * fix(repos): reuse a saved project only when git shows it's this clone A saved project outlived its folder, so Clone reported success without running git. Fixes stablyai#23563 * fix(repos): let a repeat clone of an empty repository reuse its project A clone of a repository with no commits leaves HEAD unborn, which `rev-parse --verify HEAD` rejects exactly like the `refs/heads/.invalid` HEAD a killed clone leaves. The saved-project check read both as "not a finished clone", so cloning a brand-new empty repo a second time — a double-clicked Clone, a re-run `orca project setup-clone`, or stablyai#2981's queued retry — fell through to git and died on "destination path already exists and is not an empty directory". Ask the two questions apart when `--verify` fails: `symbolic-ref HEAD` resolves an unborn branch but not a killed clone's HEAD. It is already on the relay's read-only allowlist. Probes also short-circuit once the clone is cancelled, so no probe after Cancel spawns git. * fix(repos): prepare the worktree root after re-cloning a project's folder The branch that re-clones into a saved project's folder returned the project without the two steps every sibling branch runs: the worktree root that vanished with the deleted folder is never re-created, and the authorized-roots cache keeps the answers it cached while the folder was missing. Creating a worktree or reading files in the recovered project then fails on a root that is not there. * fix(ssh): register the root after re-cloning a saved project's folder `addRemoteRepoFromPath` returns an existing project before it notifies `session.registerRoot`, which was unreachable before the saved-project check could re-clone into a project's own path. The relay therefore never learned the folder came back, so sessions under it stayed unregistered. Notify from the clone, mirroring the folder-to-git upgrade beside it. * fix(repos): say which reason refused a clone over a saved project One message covered both "that project holds a different repository" and "Orca has no record of what that project held", which need different things from the user. Name the recorded URL when there is one, and say plainly that there is no record when there isn't — the second is the case a project predating remote-identity enrichment lands in, and the user cannot act on it without being told why. Also drops the `upstream` remote-name check into a named binding: `origin` is the only stored remote that names a project's own repo, because `Repo.upstream` and the GitHub avatar derived from it both name the repo a fork came from. * fix(repos): bound saved-clone probes and prove empty clone completion * fix(repos): keep clone validation host-scoped and locale independent --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…ablyai#25828) * perf(ci): share release JavaScript across packaging hosts * fix(ci): verify the projected web entry in release archives * fix(ci): use the Windows system archive tool for release bundles * test(ci): retain stylesheet evidence in release build comparisons * test(ci): verify release parity across native color rounding * test(ci): normalize manifest asset references without changing import order * test(ci): compare portable outputs across Windows text and color formatting * test(ci): preserve module identity across dependent asset hashes * fix(ci): keep SVG build inputs identical across release hosts * fix(ci): stabilize compiler inputs and projected web bindings * fix(ci): retain vendor minification in projected web output * test(ci): normalize platform-specific pnpm manifest source paths * fix(packaging): exclude shared build staging from application files * test: align thinking-state fixtures with the current source shape * test(mobile): reuse message fixtures within the line limit
…ablyai#25744) * refactor(codex): remove trust-grant code the ~/.codex reconcile left unused * refactor(codex): remove the native trust-grant host only tests reached Only WSL runs a grant session, so resolveNativeCodexTrustGrantHost, its timeout, the native host arm and buildNativeCodexBinaryStamp go. The ledger still parses a native stamp, so files an older build wrote keep loading. The grant suites now run on a WSL host with the guest probe stubbed. * docs(codex): name the live caller in the envToDelete comment * refactor(codex): the trust-grant host key and telemetry lane name the one host left Only WSL reaches the grant, always on the managed lane; the telemetry event keeps its shape. * refactor(codex): remove the unwired real-home lane gate * docs(codex): the trust-grant telemetry comment names the one surface that still grants
…stablyai#25938) Repair deleted Active Server defaults through validated registry reads and existing settings notifications. Capture editor ownership from backing files so workspace focus cannot redirect file operations. Correct two live-turn test fixture calls to the current API. Refs stablyai#21489. Historical session recovery remains separate work. Co-authored-by: Chen <zwq19980411@gmail.com>
Recover PDF previews after failed refreshes, clear previous pages on changed contents, and preserve scroll and zoom while releasing superseded resources. Co-authored-by: sotashimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
* Fix Claude interruption status with native terminal evidence * Fix relay evidence test wiring and current host-turn test inputs * Fence Claude interruption against continued work and replaced PTYs
* Keep profile saving alive after a stalled main loop After a long main-loop stall (overnight sleep, dark wakes), the profile writer's overdue 30s timeout could run before an acknowledgement that was already queued, permanently retiring the writer until restart. Terminal creation then failed because pane bindings could not be saved. - Writer deadlines measure lateness on the monotonic clock and grant a bounded fresh window when the callback is overdue or the system reports suspended; resume re-arms without spending grace. Applies to initialization, every command, and the close/exit wait. - The "Saving stopped" alert is parented to a visible main window (never a parentless synchronous macOS alert), deferred until shown, deduplicated, and says whether the latest change is unconfirmed. - Timeouts, grace, writer faults, and alert presentation leave sanitized durable breadcrumbs. * Fix profile writer timeout and shutdown races * Run profile writer stall regression on Linux and Windows * Keep Electron probes out of headless runtime qualification
…n relaunch (stablyai#25711) * fix(terminal): keep a pane closed just before quit from coming back on relaunch The daemon now lists a session whose kill it accepted as state 'exiting' (still live), the inventory carries that through to pty:listSessions, and the worktree activation gate never adopts an exiting PTY into a new tab. Co-Authored-By: Claude <noreply@anthropic.com> * test(e2e): use the Linux close-pane chord (Ctrl+W) in the closed-pane relaunch spec --------- Co-authored-by: Claude <noreply@anthropic.com>
…5721) * fix(native-chat): honor saved agent launch commands and arguments * fix(test): retain provider import after syncing main * fix(native-chat): preserve launch arguments and explain invalid settings * refactor(native-chat): require the current launch arguments source * fix(native-chat): require saved arguments when installing the runtime * fix(native-chat): show saved argument refusals beside launch retry * fix: ship saved-argument error messages in every locale * test: align model discovery with executable overrides * chore: drop the local pnpm install lockfile block
… written when the surface exists (stablyai#24934) Step 1 of the agent-launch unification, on main. - The dispatcher stamps every request's caller from what its connection proved (runtime socket: the local CLI; the desktop's IPC: the desktop; a paired socket: its device). Params never set it. - The launch record is written twice: once when the tab exists (what creation settled: on the launch command, a draft, or a submit still `unconfirmed`), and again once the prompt's fate is known. A restart in between finds the running agent instead of answering "unknown". - A replay re-derives its terminal handle from the pane key in the running host, and shows `unconfirmed` only to callers that advertise agent.launch.prompt-unconfirmed.v1. - The record store opens in its own slot, without building the chat host; the chat host is built on that same store. Rebuilt from this PR's own commits (b9adf88, 0d7d9b5, c83f44d, 39d15d5) onto main, without stablyai#24080/stablyai#24081. Conflicts: the delivery doc table (main's "line fits" row plus the `unconfirmed` row), and main's journal-database open in install(), which now goes through the record-store slot.
…e stop row after it (stablyai#25051) * refactor(native-chat): derive the queue's pause from Stop and Resume journal rows Stop now appends one journal row where it takes effect, before the interrupt, whatever the queue holds; Resume appends its own. The pause is a pure function of the fold: the latest Stop with no later Resume and no later accepted turn a person asked for. A /clear's carried cards name their source, which is the replacement's 'cleared' pause. Host-origin turns never lift either. One predicate decides which cards a pause holds; by default every waiting card without a hold of its own, including one queued after the Stop. The drain's consume re-judges it inside its own transaction. The rows are tombstones of an id no item takes, carrying the mark: a released host reads an unknown row kind as corruption and truncates the journal there. Deletes the stored pause (recordPause, the retire hook on every appended row, the settle-before-record step, mayReturnToWaiting and its row overlay) and the tests that only proved it retires. The queued_message_pauses table stays in the schema, unread and unwritten, for downgrade safety. * fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones A Stop's pause now holds only the cards queued before its row, plus a steer it withdrew, which returns to its own place. Each card records the journal position it was queued at, and the one hold rule compares that with the Stop row. A card queued after the Stop is a new instruction: it sends as usual, but the drain still stops at the first held card, so it never overtakes them. /clear's pause holds the cards it carried. Holding every card again is a one-line switch in that rule. * fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction The drain's pick and its consume now read one function, nextSendableQueuedCard, so a Stop row that lands between them holds a newer card behind an older held one exactly as the pick would. Notes why Stop and Resume ride a tombstone row. * fix(native-chat): stop creating the unused queue pause table The queue's pause is derived from journal rows, so nothing reads or writes queued_message_pauses. It was still created on every open "for downgrade safety", but an older build creates it itself when it opens the database, so the table only sat empty in every new database. The tests now pin that no pause table exists. * fix(native-chat): a Stop's pause never hides the restart pause A Stop holds only the cards queued before it. The pause derivation still returned the Stop alone whenever it was in force, so the restart pause was never considered: a card queued after the Stop, written by a host process that has since exited, sent by itself after Orca restarted, with no pause header and no Resume. A /clear pause that held nothing could hide it the same way. Every pause in force is now derived. A card is held if any of them holds it, and it names the first that does. The drain's pick, the consume transaction's re-check and the published header all read that one rule; the header names the pause holding the first card Resume would send. * test(native-chat): pin the Stop's no-resend, lift and held-card rules - The Claude and Codex Stop-withdraws-a-steer tests checked "not sent again" at one instant, before a queue ignoring the pause re-sends. They now wait for the stopped turn to end and re-check after a quiet window. - The deleted-card test read a card queued after the Stop, which sends whether or not a person's turn lifts it; it now reads the Stop's pause before and after that turn. - Unit cases pin that a Stop holds a card with no recorded position and one queued before a rewind. * refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller The Stop row that paused the queue becomes the general Stop event { reason, turnId?, at, caller? }, whose reason is the host's existing stop cause. It still rides a tombstone of a host-only id (a released host deletes the journal from the first unknown row kind), and Resume keeps its own marker on its own id. Only a person's Stop (reason user-stop) pauses the queue. * test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event * test(native-chat): pin that Stop and Resume rows never reach apps or count as history * test(native-chat): only a person's Stop event pauses the queue * test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop Through the real host: the event names the turn and who asked and is in the journal when the interrupt reaches the agent; at an agent still starting it is there before the start is ended and holds a card queued before it; an idle Stop writes one only when it withdrew a send; and the queue's claim re-judges a pause that landed after its pick. * test(native-chat): a card held at a starting agent is checked before the Stop's timing Also says precisely what the claim's in-transaction pause check defends against: the Stop and the drain share one serialized lane. * test(native-chat): a released build keeps and folds a journal holding Stop events Replays this build's rows from the released build's own journal database: every row is kept, the history after the Stop still folds, and an older client is sent only removed ids no item uses. * style(native-chat): format the Stop event changes * test(native-chat): type the released build's exports through one checked helper * fix(native-chat): the Stop/Resume row guard narrows to those tombstones only * test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade The Stop-event downgrade test ran in no CI lane: unit shards exclude the cross-version folder, and the cross-version lane runs a fixed file list that did not name it. It is now on that list. Its only case replayed the rows into a release's own fresh database, because that release cannot open the current host database. A second case opens the journal this build wrote with a main build that shares the database: it opens writable, keeps every row, appends, and this build then reopens it with the person's Stop still pausing the queue. * fix(native-chat): a Stop that stops nothing new writes no Stop event A Stop reaching a running agent wrote a Stop event on every press. Two presses before the first interrupt landed wrote two events, so a card queued between them counted as before the latest Stop and was held, though a card queued after a Stop should send normally. A Stop naming a turn that had already ended, as a phone sends late, also wrote an event for a turn it never stopped. It now writes one only when it withdrew a queued send, or stops something no event records yet: not a turn the journal no longer runs, and not the live turn a Stop still in force already names, unless a card was handed over into it since, which this Stop's interrupt sends back and must hold. The interrupt and the "already finished" note are unchanged. A Stop at a starting agent still always writes. * test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop * chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled * fix(native-chat): any later Stop event ends a person's Stop pause A person's Stop paused the queue until their next accepted turn or Resume, and a later Stop of another reason (the host stopping the agent, an eviction, a close) was ignored. Now the pause is the latest Stop event's: a later Stop of any reason ends a person's pause, and only a person's Stop pauses. The fold keeps the latest Stop event whatever its reason. An eviction of a resting chat writes no Stop event (a Stop that stops nothing writes nothing), so it cannot release held cards; a test pins that no event means no lift. * fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed A Stop pressed before the agent's turn shows in the journal (before Claude's echo, or before Codex opens the turn) records no turn. A second press once the turn showed compared that missing turn with the live one, wrote a second Stop event, and held a card queued between the presses. A repeat is now judged by what was sent since the Stop in force: with nothing sent after it (a refused send aside), a Stop that named no turn, or named the live one, is repeated and writes nothing. Anything sent since and not refused, including a send whose fate is unknown, makes the new press write, since its interrupt may send that card back to waiting. Tests: the two-press case across the turn showing; a steer between the presses settled unknown; and a Stop naming a turn that ended while the next card is sent but shows no turn yet, which writes and holds that card. The fold test that claimed an eviction path is renamed. * fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes A Stop or Resume row's value is read from disk with no shape check, and the pause fold stored whatever it found. A stored `stopEvent: null` would then throw on every pause check for that chat: the queue's pick, its send, and every queue update to clients. No build writes such a row, so this is hardening. The fold now reads a Stop only when it is an object with a string reason and a finite time, and a Resume only when it is `true`. Anything else is ignored: it pauses nothing and ends nothing. The row is still not treated as malformed, which could cut the history short. * fix(native-chat): a Stop still reads as yours after Orca restarts before the turn ends Every stop that ends work now writes the Stop's event before it ends the child: a person's close of the chat, an eviction (worktree teardown, orchestration stop, tab cleanup) and the idle sweep's stop of a start that never landed. A stop that ends nothing writes nothing, and quit writes none: its resume marker records why. The turn-end write reads the latest Stop event where every turn row is built, so the adapter's settle, the host's fallback and the relaunch's settle all agree: a turn a person's Stop or close named, ending with no verdict of its own after that Stop, ends as their cancellation. A relaunch's probe-bounded end is no earlier than a Stop that found the turn running. When the provider refuses the interrupt and the turn runs on, a refusal row answers the Stop, so a later crash still reads Failed; pressing Stop again after a refusal is a new Stop. * refactor(native-chat): a stop no longer carries its cause; the turn's end reads the Stop event The cause of a stop was threaded in memory from each entry through the host's stop step, the adapter router and each adapter's close onto the `ended` it settled with, and Claude kept a per-turn copy of a Stop it sent. All of that is gone: adapters settle a turn they cut as interrupted with no verdict, the host's fallback does the same, and the one rule where a turn row is built (`turnEndAfterStop`) reads the journal's latest Stop event to say whether it was a person's. - `closeSession` / `disposeSession` take no cause; `ended` has no `stopCause`. - Claude reads an error result after a person's Stop as their cancellation from the journal's Stop event (through the event sink), not from a per-turn slot, and a refused interrupt is the host's refusal row, not `withdrawTurnStop`. - An owed wind-down keeps no cause: its retry's fallback reads the Stop event. - The mutation context's Stop passes no cause: its step already wrote the event, and the delivery loop's child-end reason is read back from it. - A Stop pressed before its turn showed applies to the turn that opens under it, unless a send a person made since was accepted. * test(native-chat): a turn a later send opened is no Stop's that named no turn * test(native-chat): the restart test's death proof carries its detail * refactor(native-chat): a refused Stop leaves no record; a Stop only ever ends the turn it names The stop-refused mark is gone: its tombstone kind, its fold, the clock-keyed match that tied it to a Stop, and the exception that let a second press after a refusal write a new Stop. A Stop that stops nothing writes nothing. A Codex refusal names a turn that is no longer its active one, and the Stop names that turn, so the turn running instead never reads as the person's by its id alone. * fix(native-chat): a Stop pressed before any turn showed stops only the turn opened next A Stop that named no turn read as the person's cancellation for every later turn that opened after it, until a send a person made was accepted. The queue's drain, orchestration mail and a restart continuation send as the host, so a turn they opened long after, cut by a crash, read "Interrupted" as if the person had stopped it. The Stop now applies only to the first turn opened after it. * fix(native-chat): an older Claude's error end after a Stop pressed before its echo reads Interrupted Claude CLIs before 2.1.91 end an interrupted turn with an error result that names no reason. The translator judged whether a person's Stop explained it by its own copy of the Stop rule, which ignored a Stop that named no turn, so a Stop pressed before Claude echoed the send read "Failed". The translator now writes such an end as interrupted with no verdict and no error row whenever a person's Stop may name the turn, and the journal's one rule decides as it writes the end. * fix(native-chat): a person's Stop and /clear each name why they end the agent The host's mutation path ended the agent with one "recorded" ending for every caller, which read back the reason of whatever Stop event the journal held last, however old. /clear writes no Stop event, so its end took an unrelated earlier reason. Each caller now names its own: the chat's Stop `user-stop`, whose event its own step wrote, and /clear `user-close`, the user replacing this chat. * fix(native-chat): a host stop judges whether it ends work after the provider's rows land A close, eviction or host stop decided whether it ended a running turn from the journal as it stood, while the provider's own rows (the turn its echo opened) could still be in the session's event sink. A close landing in that gap wrote no Stop event, so the turn it cut read as news. It now reads after the sink drains, as a person's Stop does, through the same check; a drain that fails or takes over a second reads working. * fix(native-chat): a Claude Stop naming a turn that just ended still marks the follow-up it cuts A phone names the turn it last saw. When that turn had ended and a follow-up was still unechoed, Claude's Stop interrupted the follow-up and ended the child, but the Stop's event named the ended turn, so the follow-up's turn the child's end cut read "Failed" under "Cancellation requested.". A Stop that ends the provider's session ends whatever is in flight, so its event now names the live turn or none, and a Stop that names none binds the turn opened next. Codex keeps naming only the turn the Stop names. The Claude Stop turn-end tests move to their own file, since the session-ending Stop suite is at its line budget. * fix(native-chat): the idle sweep reads working by the same rule as a stop's event The sweep judged a chat resting while a send whose reply was lost was still unanswered, but the stop's event writer counts that send as work. So the sweep evicted it and wrote an evict event, which ends a person's Stop pause and let the cards behind it drain on their own. The sweep's owed work now reads the main agent working the way every session list and the event writer do. * test(native-chat): an aborted eviction's injected drain failure lands on the eviction's own drain A host stop now drains the session's sink once to judge whether it ends work, so the tests that fail the eviction's drain-published step skip that first drain. * fix(native-chat): the idle sweep's rest writes no Stop event; it evicts a send that never echoes The previous commit made the sweep count an unanswered send as owed work, which pins a chat whose admitted send Codex never echoes forever, and the sweep exists to retire exactly that. That rule returns. The sweep stops only an agent it judged resting, so its eviction now writes no Stop event, whatever send it retires: a person's Stop pause holds through it. * fix(native-chat): stopping a start that carries no send writes no Stop event A host stop, eviction or close of a starting child wrote a Stop event whatever the start carried. A start with a send already reads working, so the clause only mattered for a start with none, which ends no turn and no send: its event only lifted a person's Stop pause and bumped the idle clock, which is why the idle sweep had been changed to close the conversation in the same pass. The clause goes and the sweep is #24072's again. The child's end still reads host-stop, as before. * test(native-chat): a Stop's pause across a restart is tested with a restart that writes no event The rig's restart closes the chat with an eviction, which now writes a Stop event when work runs and so ends a person's Stop pause. "A Stop never hides a restart's pause" then passed with no Stop pause left to hide anything. Those tests, and the pause-lift test whose dropped assertion returns, restart as a process that dies with no close, which like a quit writes no Stop event, and assert that both the Stop's and the restart's pauses are in force first. * fix(native-chat): a host stop of a turn a person's Stop is still ending keeps that Stop's reason An eviction or host stop that landed while a person's Stop or close was already ending the same turn wrote a newer Stop event, and the turn's end reads only the latest, so the person's Stop of that turn read as news. A host reason now writes nothing while a person's Stop still decides what runs: the live turn it names or bound, or, with none, the turn a send opens next. The person's own close still writes. The E2 tests now open and end the stopped send's own turn, as Codex does, so the mail turn after it is not the turnless Stop's. * fix(native-chat): an older Claude's error on a later turn keeps its error text after a Stop The translator left an error result that names no reason to the journal's Stop rule whenever a person's Stop named the turn or none, but the rule binds a Stop naming no turn only to the turn opened next. So a real error on a later turn read "Failed" with its error text dropped. The translator now asks the journal's rule itself (`personStopDecidesTurn`, the one core `turnEndAfterStop` and a host stop's in-force check share), so the two cannot disagree. * fix(native-chat): a Stop of a start that never landed binds no later turn, whatever sent it A person's Stop pressed while the agent starts names no turn, and the send it stopped is cancelled before it opens one. The Stop then bound the next turn anything opened (orchestration mail, a restart continuation, the queue's drain, all of which send as the host), so a host eviction of that turn wrote nothing and its crash or close read as the person's cancellation. A Stop that named no turn now binds only a turn no send journaled after it opened: any send since, of any origin and not refused, opens its own. The E2 test's mail send is accepted as Codex accepts it, instead of opening the stopped send's own turn first. * test(native-chat): a rewind's restated turnless Stop binds no turn opened after the rewind A Codex rewind restates a person's Stop still in force after the turns it keeps, at a new sequence, so by sequence alone it would bind the next turn opened after the rewind. A send journaled after the restated row voids that binding (the previous commit), which this pins. * fix(native-chat): a relaunch settles a person's stopped turn with no "stopped while in progress" row After a restart, a turn a person's Stop ended reads "Interrupted after N" with the muted mark, but the relaunch still added the error row saying the provider stopped mid-response, which a live Stop never writes. The settle now skips that row when every turn it interrupts is the person's Stop's by the journal's one rule; a crash nobody stopped keeps it. * test(native-chat): the unexpected-exit settle's journal fake answers whether a person's Stop decides a turn * fix(native-chat): a host stop whose sink drain fails reads the journal as it stands A host stop drains the session's sink before judging whether it ends work, and a failed or slow drain read as working. So an eviction of an agent at rest wrote a Stop event that ended nothing, which lifts a person's Stop pause, and a close wrote a person's event naming no turn. The drain is now best effort: the stop goes ahead either way and only its record is at stake, so a failed or slow drain leaves the journal's read as it stands. A person's Stop keeps its own rule. * fix(native-chat): a Stop that named no turn applies only to a turn a send it stopped opened A person's Stop pressed before any turn showed names no turn. It bound the first turn opened after it, then (5ead1f6bccd) any turn opened by no later send, so a turn the host started for a card the Stop held, or for orchestration mail, read as the person's cancellation, and a host eviction of it wrote no Stop event when its send had been abandoned by the close first. The rule is now the concept itself: a Stop naming no turn applies to a turn opened by a send it stopped, one already handed to the agent at the Stop's position. Nothing new is stored. The turn's row names the send that opened it (Codex: the submission's key; Claude: the echo, which the journal aliases to the submission), and a handed-over send's item sits at its handover, so the target set is derived from the journal. A card the Stop held is handed over after it, so it is no target; a Stop of a start whose send never opens a turn binds nothing; a rewind keeps no submissions, so a restated Stop binds no turn opened after it. With no turn running, a host stop defers to the person's Stop only while every unanswered send is one it stopped. Claude's translator, which asks before its echo row lands, passes the send its echo acknowledged. * fix(native-chat): a host stop whose sink drain runs long reads the agent working; a failed one reads the journal A drain past its bound may still hold the turn's row, while the echo's acceptance has already landed, so the journal as it stands read nothing running: a person's close of that turn wrote no Stop event and the turn read as news. The two drain outcomes now differ: one that failed has nothing more to deliver, so the journal's read holds (as before); one still running reads working. * test(native-chat): a host stop with no turn running defers only while every unanswered send is the Stop's The branch had no test. An eviction with only the stopped send unanswered writes nothing; one with a send made after the Stop still unanswered writes its event. * fix(native-chat): a slow sink drain reads working only while an accepted send's turn row is due The previous commit read every drain past its bound as working, so a host eviction or stop of an agent at rest during a sink backlog wrote a Stop event that ended nothing and lifted a person's Stop pause. A slow drain now reads working only when the latest send the agent accepted has opened no turn the journal holds, the race it was for; otherwise the journal's read holds. * test(native-chat): the host-stop control keeps the stopped send unanswered beside the later one With both unanswered, the host writes only because not every unanswered send is the Stop's; a rule that deferred when any one was would pass the old control. * fix(native-chat): a steer is no send owed a turn when a slow drain judges a host stop A slow drain reads working when the latest accepted send has opened no turn yet. A Codex steer or a Claude fold is accepted into the running turn and never opens one, so a chat at rest whose last send was a steer still read working, and an eviction lifted a person's Stop pause. Sends delivered into a running turn, whose item carries that turn's scope, are skipped. * feat(native-chat): a chat reads Stopping from the person's Stop until the work it stopped ends The host derives it on each journal publish from the Stop's event, the live turn and the Stop's own answer, and publishes it as an optional field on the session status and the main agent's row. Clients present it: the chat's tail line and Stop control, the sidebar row, worktree ps and the phone's row. The chat and the phone also read their own Stop press until its request answers. * test(native-chat): pin Stopping on the phone and across mixed versions * test: give touched fake journals and mocks their SAFETY notes * test(native-chat): a turn waiting on the person reads attention, never Stopping * test(native-chat): the sidebar row follows Stopping when it is the only field that moved * test(mobile): the phone reads Stopping from its own Stop until the request answers * test(mobile): type the held Stop request instead of casting it * chore: keep the base lockfile (a local pnpm run rewrote it) * fix(native-chat): narrow the Stop note's optional failure; type the phone test's reply * test(native-chat): type the Stop test envelope's fields narrowly * fix(native-chat): a Stop the agent declined, or whose child end failed, says so while the turn runs on A Stop naming the turn that still runs, refused by the agent, now writes the Stop's refused fact instead of 'already finished'. A session-ending Stop whose child end fails while the work runs on revises its note to unconfirmed. * fix(native-chat): Stopping holds while any press of the Stop took A repeat press refused after an earlier press took no longer clears Stopping. Exit early when the Stop named a turn that is not the live one. * fix(native-chat): keep Stop enabled while the host says Stopping Only this client's own Stop request in flight disables Stop and Esc. A repeat Stop is how a stop the provider took but never answered escalates. * fix(sidebar): every agent row says Stopping in place of its tool line The dashboard row, which the sidebar's non-compact mode also draws, read the last tool line while a person's Stop ended the turn. It now shares the compact row's rule. * fix(mobile): the worktree list sees Stopping change on its own A snapshot whose only change was the host dropping Stopping compared equal and was thrown away, leaving the row on Stopping. * refactor(native-chat): fold the status feed in src/shared for both clients The snapshot merge and the contact-loss strip move out of the renderer feed so the phone folds the same stream the same way. * feat(mobile): let phones read the structured session status stream agentSession.subscribeStatus joins the mobile allowlist. The agent-session methods move to their own file, which the at-cap allowlist spreads in, and the allowlist test reads the Set instead of parsing the source. * feat(mobile): the phone chat reads Stopping from the host, like the desktop One status stream per client, opened on a host that advertises the status feed; a refusal to phones reads as no feed. The chat reads Stopping from the host or its own press, and holds Stop only while its own request is in flight. * test: give the new fakes checked types or a SAFETY reason * revert(native-chat): drop the refused-named-turn rewrite of a Stop's note Codex can send its refusal before the turn's end frames, so reading the turn as still live after a flush races; the Codex Stop that ends nothing is handled by ending the process instead. The base's 'already finished' note and its test expectation return. The Claude wind-down failure revise stays. * fix(mobile): release the status stream when the host ends it; refusals last one connection The feed now drops the handle of a stream the host ended or refused, so the logical client never replays it on a later session. A refusal to phones holds for one connection, so a host updated while the phone stays paired is asked again. * perf(native-chat): read the live turn's opener from its record when deriving Stopping After a Stop that named no turn, every later commit walked and copied the whole journal to find the live turn's record. The derivation now reads that record off the rendered snapshot's tail and decides with the same rule. * refactor(mobile): move the method-unavailable check into transport The status feed imported it from the Files tab's fallback. No behaviour change. * test(native-chat): a send after a Stop reads Working before its turn opens Pins the derivation's running-only read of the newest turn: the stopped turn, already ended, must not keep the next send on Stopping. * fix(sidebar): the compact row leads with Stopping so a narrow sidebar keeps it whole At the default width the row read 'Codex Chat - Stoppin…': the model and time keep their room and the line truncates from the end. Stopping now leads the line the way monitoring already does, so the chat name is what gets cut. Also pins that the turn bar keeps its running clock while the tail line says Stopping. * test(native-chat): the retry of a close whose exit was unproven writes no second Stop event The idle sweep finishes a stop left owed with that stop's own cause. It is the same stop, so its event stands alone and the child's end keeps the cause, for a person's close and an eviction. * fix(native-chat): read and write a Stop's answer by the turn its event records Stop notes are now one row per turn, keyed by the turn the Stop's event records. Performing a Stop and deriving Stopping share that key. A refused or unconfirmed answer never overwrites one that took at the same key; only a session-ending Stop's failed wind-down downgrades it, and that step now revises the note the Stop actually wrote, carried on the wind-down. Stopping reads the turn's note whenever it was first written, plus newer notes no other turn owns. Test fixtures gain the host logger and the phone's quietRepeatedStop. * refactor(native-chat): read a Stop's target once for its event and its note The chat's Stop now reads what it is aimed at (the named turn and whether the Stop ends the provider session) once, and both its event and its note's key derive their turn from that one value through the same rule. Adds the host test for a Stop naming an ended turn on a provider whose Stop ends the session. * fix(native-chat): the host never steers a message into a turn a Stop is ending A queued card's Send-now, or a send made while a person's Stop ends the turn, went to the agent as a steer into that turn. The delivery loop now holds any waiting message while the host's own Stopping reading holds, and sends it as its own turn once the turn ends. The Stopping reader also stops at the Stop's position and looks the turn's note up by key, instead of walking the whole journal. * feat(native-chat): while Stopping, the composer says a message runs after the stop Desktop and phone: the composer placeholder reads "Queue a message to run after the stop" while the chat reads Stopping, and a queued card's Steer (and the desktop's steer shortcut) is held. New key translated in all 6 catalogs. * refactor(native-chat): the chat pane's Stop controls live in their own module The pane went over its line limit once merged with main. Its Stopping reading, the press that holds Stop, and the steer and placeholder it hands the composer move to native-chat-structured-stop-controls.ts. * fix(native-chat): hold a send at its handover, reading the feed's own Stopping The hold was checked when the delivery step started, but the handover runs in a later step after waiting on the agent's start, so a Stop landing in between let a new send steer into the stopping turn. The check now runs at the handover. It reads the status feed's projection for the commit instead of rendering the journal again, so holding a send adds no journal read of its own. * refactor(native-chat): one display status decides Stopping on every surface agentStopDisplayStatus combines whether the agent works, the host's flag and this client's own press. The chat pane, sidebar and dashboard rows, and the phone's chat all read it, instead of each combining the flags. * fix(native-chat): Stopping holds until the stopped turn ends, whatever the Stop's answer A Stop the agent declined, or whose end went unconfirmed, used to drop the chat back to Working. It now stays on Stopping until the turn ends, and Stop stays enabled so a repeat press escalates. The Stop's answer is no longer read for Stopping, so its note goes back to the key the base gives it (the restore of queued-stop.ts and the removed key test landed in the previous commit). The note still keeps a press that took over a later refusal, and a failed process end still says the Stop went unconfirmed. * fix(native-chat): a Stop binds only the turn it actually stopped A Stop pressed before any turn showed used to claim, at end-write time, whatever turn the stopped send later opened, even when the Stop stopped nothing. A turn that then died on its own read as "Interrupted" (your cancellation) instead of "Failed". Now a person's Stop that named no turn binds, in memory only, every turn that ends while the Stop settles, and afterwards only the turn its interrupt took. The settle ends a still-running stopped turn once. A relaunch finds nothing in memory, so an unsettled turnless Stop binds no turn. A Codex Stop whose answered turn does not open within its wait, or whose send's answer was lost, now answers refused, so the host ends the child and the turn can never run. * fix(native-chat): keep the person's queue pause and close binding after a Stop settles A host stop or eviction with no turn running now defers to a person's Stop while its queue pause still holds with nothing sent since, read from rows, so a held card is not handed off on reopen after a Stop that did nothing or whose kill failed. A person's close that named no turn opens a settle around its child's end, so a turn that end cuts reads as theirs. A press opens its settle only when the latest Stop event is its own or the one in force it repeats: a late Stop, a card's interrupt or a lost event row reopens no earlier Stop. A Codex Stop that cannot reach a turn still able to open says the Stop is unconfirmed rather than that no turn ran, and a second Stop still reaches a turn an earlier wait left unopened. Also drops the unused openedBy plumbing and the unreachable "a written cancellation stays one" rule, and pins a relaunch after a named Stop. * fix(native-chat): keep a failed Stop's turn display-only, and settle edges off the commit path A Stop that failed marks the turn it could not stop for "Stopping…" only (JournalStopSettle.failedOn): no turn-end rule reads it, so that turn's own end with no verdict reads as a failure, not the person's. A settle edge writes no row, so it no longer goes through the journal's commit listener, which also delivers history, counts as activity for the idle sweep and schedules the queue drain. A narrow settle-edge hook republishes the status row and wakes the steer hold's handover, and nothing else. * fix(native-chat): a Codex Stop agrees on both presses when a turn is still owed, and pin the close's settle A Codex Stop that waited for a turn Codex answered a send into now answers "may still open" whenever that turn neither opened nor ended and its send is still owed, however the wait ended (it ran out, or the thread went idle). Before, a first press after an idle thread said no turn was running and kept Codex, while an identical second press ended it. Adds a test that a person's close the conversation outlives (as /clear does) closes its settle, so a later turn that ends on its own reads as a failure. * fix(native-chat): a Stop that failed before its turn showed still reads Stopping through that turn A Stop that failed with no turn open marked nothing, so the chat dropped to Working and the turn that then opened never read "Stopping…". The display-only mark now also covers that case: the first turn that opens after the Stop failed, provided no message was handed to the agent in between. No turn-end rule reads the mark, so that turn's own end with no verdict still reads as a failure. * test(native-chat): a Stop whose event row failed binds no turn to an earlier Stop With one ordered journal writer the Stop's event is in the fold when its write returns, so the press reads whether it owns the latest Stop from the fold instead of awaiting the write. Pins the case the read must refuse. * test(native-chat): name the settle, not a stream drain, in the Stop's own-end test * test(native-chat): a Codex Stop answered before Codex ends the turn reads interrupted throughout Codex answers an interrupt it took before it sends turn/completed (interrupted): on TurnAborted the app-server answers pending interrupts, then ends the turn, on one channel. The test fake did the reverse. It now answers first and ends the turn on a later read, and the tests that read the turn's end right after a Stop wait for it. New end-to-end test through the shipped host, journal and Codex adapter: with the real order, every end row of the stopped turn reads interrupted by the Stop (named, unnamed, and a Stop pressed while turn/start was in flight). Breaking the settle window turns the in-flight case red: the Stop's own end row then has no verdict, which reads as failed until Codex's end lands. * test(native-chat): Stopping ends with a Codex turn whose interrupt is answered before its end With Codex's real order (the interrupt's answer, then turn/completed interrupted), the status shows Stopping while the Stop settles, drops it once the turn ends, and never carries a verdict other than the person's cancellation. * fix(native-chat): a Codex Stop interrupts a turn Codex answered but has not opened at once A Stop that named no turn, made after Codex answered a send but before the turn opened, used to wait up to 5 s for the turn to open before interrupting, and ended the Codex process when it didn't. The stated reason, that Codex refuses an interrupt until it opens the turn, holds only part of the time: with no turn active, Codex takes an interrupt once its thread runs (turn_interrupt_inner), and refuses it with -32600 "no active turn to interrupt" before that or once the turn has ended. The Stop now sends the interrupt at once. Only on that refusal, while the turn has neither opened nor ended, does it wait for the turn to open (bounded at 5 s) and send it once more. A turn that ended meanwhile was nothing to stop. One that never opens, or that an earlier wait already gave up on, fails the Stop, and the host ends the child as before. Sends still wait for the turn to open before steering into it. The test fake models Codex taking an interrupt once the thread runs (run()). * fix(mobile): name how the phone's status stream is released in the subscription inventory Main made each inventory entry state its release; the status feed's stream is released from its subscribe params, as the session event stream is. * fix(native-chat): every Codex Stop waits for an answered turn to start, as the first did A Stop whose interrupt Codex refused as finding no active turn skipped the wait when an earlier wait, a Stop's or a send's, had already given up on that turn. Every press now waits its own bound and retries once if the turn starts, so a turn that opens during a later press is still stopped. Both presses still reach the same verdict when it never starts. * fix(codex): never steer a turn whose interrupt Codex answered Codex answers an interrupt as the turn aborts, before it sends that turn's turn/completed. In that gap the adapter still counted the turn as running, so a message handed over right after a Stop settled (the Stop's own end row already reads the turn ended) went out as turn/steer, which Codex refused with -32600 "no active turn to steer", and only then as turn/start. The adapter now marks a turn whose interrupt Codex answered as aborted until its turn/completed, never steers into it, and starts the message's own turn directly. Steering a turn that is genuinely running is unchanged. * fix(native-chat): a message sent while Stopping is queued as a card, whatever the setting While the chat reads Stopping (the host's flag or this client's own Stop in flight) there is no turn left to steer into, so the desktop asks the host to queue the send even with the queueing setting off, and it is never drawn as a bubble inside the turn being stopped. The phone already queued every send on a capable host; a test now pins that it does so while Stopping. * fix(native-chat): a message queued while Stopping is a card at once, not after the stop A person's Stop holds the session's lane until Codex answers its interrupt, and a send was admitted only behind it. By then the turn read ended, so a send that asked to be queued went out plain: no card for the whole of Stopping, then a bubble and a new turn. While the host reads that a person's Stop is ending the work, a text send that asks to be queued is admitted without waiting for the lane: the same ledger and lease admission, and a plan that only writes the card through the journal's ordered writer. The card runs when the stop lands; the Stop's pause holds only cards queued before it. Anything else, including a Stop that settled by the time the send runs, takes the lane as before. The Codex test fake now drops the active turn when it takes an interrupt, as Codex does before it answers, so a turn/start after the answer opens a new turn. * fix(native-chat): a Codex Stop that ends the child before any turn opened withdraws its send A Stop on a Codex turn that was answered but never opened ends the Codex process. That end settled the send as in doubt (unknown, recovered), and the client's outbox holds every later send behind a send in doubt until the person presses Retry, which re-sends the very message they stopped. The chat looked stuck. Codex records a prompt only once its turn has started, so a send whose turn never opened never ran. When the Stop's refusal says so (turnMayOpen), the child end now settles the unanswered sends as withdrawn, the verdict Codex's own interrupted-turn end already gives an unechoed send. The flag rides on the owed wind-down, so a retry after a failed child end withdraws them too. Claude's child end still leaves its unanswered send in doubt. * fix(native-chat): derive the withdrawal of a Codex send whose turn never opened Replaces the flag the Stop carried to the child's end, and its copy on the owed wind-down, with a reading of the journal at the settlement that lands. A Codex child's unanswered send is withdrawn when a person's Stop is in force since it was sent and no turn row ran, or was written, after it; any other end (a turn that opened, a host's close, a crash, Claude) still leaves it in doubt. A retried wind-down reads the same rows, so it withdraws the same sends. * fix(native-chat): a card queued while Stopping runs past the cards the Stop holds A card queued before a person's Stop waits under its pause until Resume. One queued after it, as a message sent while Stopping now is, was stuck behind them too, since the queue never reorders. Such a card was asked for after the Stop, so it runs when the stop lands, past the cards held only by that Stop's pause; a returned card and the restart and /clear pauses still hold everything behind them. Also: the host's own Stopping reading is gated on working, as the published flag is, so a failed Stop's mark never reads Stopping on an idle session; a send that falls back to the lane re-reads the conversation's journal there; and the end-to-end test asserts the queue's pause rather than a per-card field. * fix(native-chat): a paused queue labels only the cards it holds Since a card queued after a person's Stop runs past the cards the Stop holds, labelling every card "paused" while the queue's pause is published misreads that card. The host now marks each card its pause holds (heldByPause, a new optional field), and the desktop and phone label only those. An older host marks none, so a client keeps today's labels; an older client ignores the field. Adds a test of the desktop's own send through the real outbox: while the chat reads Stopping, the request asks the host to queue it and no bubble is drawn, on a host that advertises the queue. * revert(native-chat): defer the per-card queue pause label to the queue's rollout The heldByPause field and its labels are visible only where the host advertises the queued-messages capability, which shipped hosts do not yet do. Deferred to that rollout; the real-outbox send test stays. * fix(native-chat): while Stopping, say and show what a send does where the queue is dark Shipped hosts do not advertise the queued-messages capability, so a message sent while Stopping goes out plain: the host holds it until the stopped turn ends and then runs it as its own turn. The composer still said "Queue a message to run after the stop", and the message was drawn inside the turn being stopped. Now the placeholder reads "Send a message to run after the stop" where the host does not queue sends, and "Queue a message…" only where it does (desktop and phone, all six catalogs). A send this client made that the host has not recorded yet is drawn after the Stopping line while the chat reads Stopping, as a message held behind a running command already is; once the host hands it over it opens its own turn. Client presentation only. * fix(native-chat): keep a send in doubt when a turn was open for it The derived withdrawal read a turn as open for a send only if it still ran or was written after the send. A send steered into a running Codex turn whose interrupt failed met neither once the adapter's end settled that turn ahead of the host's settle, so it read withdrawn, though Codex drains a steer into the running turn and may hold it. A turn that ended after the send was handed over was open for it too: such a send stays in doubt, as before. Pins that case, and that a send made after the Stop, to a child that then dies before its turn opens, stays in doubt. * fix(native-chat): restore the per-card queue pause label Kept after all: a paused queue labels only the cards it holds (heldByPause), which is visible only where the host advertises the queued-messages capability. * fix(native-chat): draw only a send made while Stopping after the Stopping line Every send the host had not recorded yet was drawn after the Stopping line, including one made just before the Stop, which the host steers into the turn; it then jumped up into that turn once recorded. The outbox now marks a send made while the chat reads Stopping, and only those wait after the line. * fix(native-chat): withdraw a Codex send by whether it started its own turn, not by timing Whether a turn was open for a send was read from end times: a turn that ended after the send's handover counted. A send made while a Stop ended the turn is handed over once that turn reads ended, yet Codex's own end for it can arrive later, so such a send whose own turn never opened read in doubt again, and the chat's queue held behind it. The handover already records where the send went: its message joins the turn running then (a steer) or belongs to no turn (it starts its own). Only a send that started its own turn, with none opened since, is withdrawn; one that joined a running turn, or has no recorded place, stays in doubt. The Codex test fake takes an answered interrupt as Codex does, dropping the turn before its end arrives. * refactor(native-chat): move queue-while-stopping to its own follow-up The queued-messages capability is off on every shipped host (#21062), so the parts of this PR that act only when it is on move to a follow-up stacked on this one: admitting a queued card while a Stop holds the session's lane, a card queued after a Stop running past the cards it holds, the per-card pause mark, and asking the host to queue a send made while Stopping. This PR keeps the Stopping state, the host's steer hold, the rule that never steers a turn whose interrupt Codex answered, and what a send while Stopping looks like where the queue is off. * fix(native-chat): leave no Stop row when the Stop took back a send that never ran A Stop on a Codex send whose turn never opened ends the child, and the child's end takes the send back into the composer. The Stop still wrote "Cancellation requested." at the conversation level, so with the send gone it sat under the previous finished turn and read as if that turn had been stopped. A Stop that found no turn running and whose child end took back every send it found now writes no row; a Stop of a running turn, or one that leaves a send in doubt, still does. * feat(native-chat): keep a message a Stop took back on screen, with one stop row after it A Stop that took back a message before the agent started it used to remove the message from the transcript and paste its text back into the sender's composer. Other devices just lost it, and with the message gone there was nowhere for a stop row to sit, so it either landed under the previous finished turn or was left out. The message now stays where it was sent, on every client, followed by one row, "Stopped before the agent started" (one row after several taken back together). A message whose turn had opened stays in that turn, whose "Interrupted" header already says it was stopped. A queued card's hand-off is still held by the card. Nothing the host recorded refills the composer any more. Text this client's own Stop took before the host ever had it still comes back, and only into an empty composer. Derived from facts the journal already holds (the rejected submission's cancelled verdict and its user item); nothing new is stored or sent. * fix(native-chat): one stop row per stopped send, drawn where the Stop took it, and nothing lost - A message this window's Stop took before the host had it, which the composer cannot take (it holds text or images, or no composer shows the chat), now stays in the outbox as not sent, on its Retry, instead of vanishing. - A Codex send whose turn opened but was never echoed belongs to that turn: its interrupted end is the stop, so no second row is drawn. A send taken back after a later turn started still gets its own row. - A steer the Stop took back stays in the turn it joined, with no row of its own. - A queued send taken back before its hand-over is drawn after the turn it waited behind, not inside it. - The conversation outline the host serves still leaves out sends a Stop took back, and the rail gives them no tick, so what the host publishes is unchanged. - The row is drawn as the Stop's own status rows are, and never folds behind a settled turn on a host that states no turn scopes. * fix(native-chat): draw a stopped follow-up after the turn that opened while it waited A follow-up sent just before the agent opened its turn, then stopped, drew its "Stopped before the agent started" row above that turn's own "Cancellation requested." whenever the turn was expanded. A send a Stop took back is now drawn after the rest of every turn it waited on: the one it was accepted behind, and any that opened after it and before the Stop took it back. Also corrects the stoppedBeforeStart doc to what sets it. * fix(native-chat): place a stopped send by journal order, not by turn start time The turn a stopped send waited on was picked by comparing the turn's start time with when the Stop took the send back. A Codex resume rewrites every finished turn's start time to Codex's whole seconds, so after a restart (or a child respawn) a turn opened for a later send could read as started first, and the stopped send dropped below that whole later exchange. A turn is now waited on when something sent before the stopped send opened it, read from the journal's own order only. Covered end to end through a restart whose resume reports whole-second start times. * test(native-chat): pin a stopped send held behind the first send's answer A second send made while Codex still held the first send's turn/start answer, then a Stop: the first turn opens after the second was accepted and streams after the Stop. The stopped second send and its row are drawn after that turn's output, never between the turn's header and its output. Fails on the two earlier placement rules. The restart case moves into the same file. * docs(native-chat): say why the pre-echo anchor still compares times * docs(native-chat): mark the pre-echo anchor's time compare temporary (STA-9337) * docs(native-chat): say the placement rule reads journal position, not send time * fix(native-chat): keep sends a Stop took back in the order they were sent Two sends queued during /compact, the first handed over and the second not, then a Stop that took both back: the handed-over one sat at its handover row and the other at its acceptance, so the second drew above the first. Among sends a Stop took back, a later-sent one is now drawn no earlier than just after an earlier one. Sent order is submittedAt, the host's accept time and the order every client already keeps submissions in, because a client never sees a handed-over send's acceptance position (STA-9337). * fix(native-chat): draw a stopped send below everything sent before it A send a Stop took back that was never handed over could still draw above an earlier send's whole exchange when that earlier send was not itself taken back: three queued during /compact, the first ran and finished, the second was left in doubt, and the Stop took back only the third. A stopped send is now drawn no earlier than the latest loaded row of anything sent before it, and the turns it waited on are read from that point, so the earlier exchange stays above it. * fix(native-chat): count a turn opened by the floor row as one the stopped send waited on When the latest row sent before a stopped send was itself a turn's opener, that turn compared equal to the floor and was not counted, so the stopped send landed inside the earlier send's exchange: two queued during /compact, the first handed over and streaming, then a Stop took back the second. A turn whose opener sits at or before that point now counts, and the send draws after the whole exchange. * feat(native-chat): publish each submission's journal positions A client never learned where in the journal a send was accepted or taken back, so drawing a stopped send had to guess from its own row and from clocks, and the guesses misfired after restarts. Each projected submission now carries `submittedSequence` (its submission row) and `resolvedSequence` (the dispatch row that resolved it; absent while pending): two optional fields, computed on every fold from rows already stored, never stored themselves. Older clients ignore them. The failure-fact schema moves to its own module to keep the journal schema file within its size limit. * fix(native-chat): place a stopped send by the journal row that took it back Hosts now publish where each submission and its resolving row sit in the journal (cherry-picked from #25073). The projection places a send withdrawn before it started at that row, keeps stopped sends in their published submission order, and falls back to the send's own row on hosts that publish neither. * test(native-chat): pin a submission's resolved position on the provider-echo accept path Also say which rows can resolve a submission, and that the submission schema, not the shared type, omits acceptedSequence. (cherry picked from commit 58a1ade5048669b1aadbac65799a6913152a9981) * fix(native-chat): keep the previous placement rules for a host that publishes no positions On a host from before the published journal positions, a stopped send is again drawn after the latest row of anything sent before it, as before the switch, so it no longer draws above the earlier exchange. Temporary until a host version floor. Also pins the published row against tied accept times. * perf(native-chat): place stopped sends in one pass, and skip it in chats with none The message projection runs on every streaming commit. It now builds turn anchors, the item lookup and the older-host floor only when the chat has a send a Stop took back, and places each such send with a binary search over the turns' furthest rows instead of rescanning every item per send. Draws are unchanged. * fix(native-chat): count a send whose answer was lost when a Stop takes it back The no-row rule counted only pending sends, but the child's end also takes back a send this process left in doubt when Codex's turn/start answer was lost. That case still wrote "Cancellation requested." under the previous turn. Both now read one predicate, so they cannot drift apart. * test(native-chat): pin which sends a Stop's child end can take back A send an earlier process left in doubt is never withdrawn and never holds the row back, and a queued card's send is never counted. * fix(native-chat): read the host's Stopping beside main's startup phase Main now reads only the startup phase from the status feed and no longer publishes which child is starting. The chat reads the host's Stopping from its own hook beside it, and the Stopping bridge test mocks the execution-host lookup main's owner resolution now calls. * test(native-chat): phone draws the same from merged frames as from a fresh snapshot A long chat whose last send never opened and was taken back at the child's exit, right after a send made while a Stop was ending the turn before it. From every subscribe point, at every later frame and with frames that carry one to three rows, the phone's list built from merged frames must equal the list built from a fresh snapshot of the same journal. * test(native-chat): widen the merged-vs-snapshot guard to content, long chats and the phone hooks The guard compared row ids and turn bars only, never left the phone's first window, put each row's writer fence on every batch, and stopped at the fold. It now: - compares each row's content, so a phone that kept an earlier revision of an answer or a status fails it; - adds a chat longer than the phone first loads, compared from the fresh page's first user row on (a turn whose user row the page cut off is keyed differently there); - sends batches at the subscriber's fence and a fresh snapshot when the fence moves, as the host's attach does; - writes the send made while stopping while the turn still runs, adds the provider's echo of each send, and a status row the provider drops; - adds a hooks test: a phone that sent the never-opened message itself, holding its echo and reading the host's Stopping from the status feed, shows the same list after every frame as a phone that opened the chat fresh then. * test(native-chat): pin the stop row to every frame from the one that took the send back Both guards compared a merged phone with a fresh one, so a list bug they shared passed, including one that held the stop row back until the next send. Both now check, at every frame from the take-back on, that the list itself shows the stop row: the data test through the list's own turn disclosure, the hooks test on the live and the fresh phone. Also: - the send made while stopping is written after the stopped turn's end, as the QA journal shows, and while the turn still runs, as a second case; - the long chat revises and drops a status near its end, inside the window; - each phone reduces its own copy of every frame; - Stopping is checked on the never-opened send's own Stop; - the headers say what neither guard covers; - the mobile production typecheck leaves *.test-fixture.ts to the tests' program, as it does tests, since these fixtures import desktop host code. * test(native-chat): check the stop row in the chat view's FlatList, through the real overlay The hooks guard rebuilt the overlay and chat view by hand, so a memo in the real view that held its list back until the next send passed it. The guard now renders the real overlay and chat view from the phone's own hooks, and checks the stop row in the FlatList's data at every frame from the one that took the send back. It also subscribes once before the history's last dropped status, so removals run through the real hooks too. * test(native-chat): compare the live and fresh phone on the chat view's own FlatList The hooks guard compared a hand-built copy of the overlay and view, so a memo in the real ones keyed on list lengths kept stale rows past it. It now compares, after every frame, what the real chat view hands its FlatList: each row as its renderItem draws it, with its bar, and the footer. The hand-built copy is gone. It also checks that the FlatList's keys never collide and that a changed list never arrives in the same array, and the controller stub's SAFETY note now states what the stub really provides. * test(native-chat): read the outbox reconcile from where main moved it * test(native-chat): pass the projection's options and mock the phone row's new imports after main's rejected-in-place rows * refactor(native-chat): place a taken-back send by where the host now puts its row #24710 moves a rejected send's row, a Stop's take-back included, to the row that rejected it, in no turn. Draw it at that row, past the end of every turn opened before it, and keep the send-order rule. A send whose turn opened before the provider echoed it is claimed by that turn's record when it was sent before the record and taken back after it, by submittedSequence, and is drawn as the turn's opener. The steer branch goes: the host no longer says which turn a taken-back steer joined, so it is drawn after that turn with its own row. The older-host fallback (floor, clock compare, submittedAt order) is now gated on submittedSequence alone and marked Temporary. resolvedSequence is gone, as in #25073. * test(native-chat): a turn recorded before a send was made is not the turn it opened * test(native-chat): a message a Stop withdrew is drawn with its stop row, not hidden #24710's tests pinned a withdrawn send as hidden, the rule this PR replaces. Each keeps what it is about: a withdrawn copy does not hide a failed one, no delivery notice is raised, and a message the host failed to deliver stays hidden on the phone. * fix(native-chat): a retried message no longer waits behind a later Stop Retry dropped the Stop it had outlived but kept the mark that it was sent while a Stop was ending a turn, so a retried message waited behind whatever later, unrelated turn a Stop was ending. Retry is a new send: drop that mark too. * feat(native-chat): publish the turn a withdrawn Codex send was answered into Same change as #25073 at 93749070e48b, so this stack builds on it. * feat(native-chat): say how a withdrawn Codex send joined the turn it names Same change as #25073 at ab2826ee1854, so this stack builds on it. * fix(native-chat): open a turn for a stopped send only when the host says it started it A send a Stop took back after its turn opened but before Codex echoed it is drawn as that turn's opener when the host names the turn and says the send started it. A steer into that turn keeps its own row after the turn. The guess from journal order and times is kept only for hosts that publish neither field, and never hands a turn the provider resumed on its own to a send. A page that starts inside such a turn draws the send before the turn's first loaded row. * test(native-chat): an echoed opener keeps its turn from a send the host says started it * feat(native-chat): state on every rejection whether it names a turn Same change as #25073 at e02f98add56b, so this stack builds on it. * fix(native-chat): place a stopped send from older history by journal order A rejection written before the host named turns carries no answered turn, though a newer host still publishes where it was sent and has moved its row to the take-back. Such a send is matched to its turn by journal order again, as before the host named turns; a rejection that states no turn opens none. * docs(native-chat): say an unreadable answered turn reads as none Same change as #25073 at 4ab6cc431c01. * refactor(native-chat): order stopped sends without non-null assertions One helper sorts by the published send position when every entry has it, else by accept time, and both stopped-send sorts use it. The rule kept for rejections written before the host named turns is labelled as kept for good: it draws those rows exactly as before. * docs(native-chat): say which half of the older-history turn rule is kept for good * fix(native-chat): word a send after a Stop by whether this send will queue The 'queue a message to run after the stop' placeholder read the host's queue capability alone. A send queues only when the host queues and this send asks it to: the queue setting is on and no pending prompt blocks the queue. Desktop and phone now word the placeholder from that same decision their send uses. * test(native-chat): one test per case for the words of a send after a Stop * refactor(native-chat): the dictation hook owns the composer's dictation state Keeps…
…i#25724) * feat: generate chat names through configured text agents * Project structured chat names across stored tabs and session rows * Name native chats from their first live message * Restore the journal test provider handle import * Fix first-message naming and live Vault title updates * Preserve Unicode characters in bounded chat naming prompts * Read chat naming settings only when a turn needs them * fix(chat): store only generated conversation names * fix(chat): keep ordinary labels across unnamed chat surfaces * fix(chat): preserve naming after fast first turns * Preserve native chat names across command-first sends and Vault lifecycle * Route native chat SQLite contracts through the existing Node test pool * feat(settings): add chat naming controls to Chat page * Preserve chat naming drafts and configure Custom commands through host settings * Scope synthetic command output to its journal thread in naming test * Keep naming test fixtures within their typed project boundaries * Resolve the real Vault hook directly in its integration test * Publish chat naming save refs after render commits * fix: keep Japanese chat naming copy stable during repair * Update test contracts for the naming integration * Keep journal fixture reads on the host clock * Give Chat names a separate settings section
…t so main typechecks (stablyai#25977)
# Conflicts: # config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs # src/renderer/src/components/diff-comments/DiffCommentPopover.tsx # src/renderer/src/components/right-sidebar/checks-panel/active-content.tsx # src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-controller-state.tsx # src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx # src/renderer/src/components/ui/textarea.tsx # src/renderer/src/lib/worktree-activation.ts # src/renderer/src/store/slices/ui/ui-slice-preference-actions.ts
…'s Vitest launcher now needs
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1.4.214 → 1.4.214 (
2b0ce17514..cc6006779f, 164 upstream commits, eight conflicts)package.jsonstill says 1.4.214, so the branch name repeats a fourth time.config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs— upstream deleted it([CI] Reduce CI work by removing low-value tests and duplicate fuzz comparisons stablyai/orca#25791, "Remove low-value test inventories"). Took the deletion, so the fork's two workflow
entries in it are gone with it; the earlier note about re-adding them no longer applies.
DiffCommentPopover.tsx,right-panel-comment-composer.tsx— upstream fix: prevent IME Enter from submitting text fields across Orca stablyai/orca#25480 swapped their<textarea>forImeTextarea(IME Enter no longer submits). Both boxes are the fork'sReviewMarkdownComposer(TipTap), which has no textarea, so kept the fork's files whole.checks-panel/active-content.tsx— the same fix: prevent IME Enter from submitting text fields across Orca stablyai/orca#25480 import beside the fork'suseEffect. Kept both.use-checks-panel-controller-state.tsx— upstream droppedpanelVisibleSinceRef/foregroundedUnrenderedReviewKeyRef; the fork had droppedcommentsRef.current = comments(thecomment list comes from
usePRCommentsState). Dropped both.ui/textarea.tsx— upstream added acellvariant beside the fork'sseamless. Kept both.lib/worktree-activation.ts— upstream moved the sidebar-filter lifting intoworktree-activation-sidebar-filters.ts(liftSidebarFiltersHidingWorktree). Took the move; thefork's
revealTemporaryCheckout(state, wt)now sits at the end of that function.ui-slice-preference-actions.ts— upstream moved the agents-view defaults intocreateAgentsViewPreferenceActions. Took it and kept the fork'screateUiWorkspaceStatusRuleActionsimport and spread.structured-agent-session-codex-stopped-send-order.test.ts(missing
resolveLaunchArgs); upstream fixed it one commit later (fix(test): restore main typecheck — pass resolveLaunchArgs in the Codex stopped-send-order test stablyai/orca#25977), so merged again.config/bun-version) and refuses to start withoutit, so
fork-verify.ymlinstalls it withoven-sh/setup-bunbefore the fork's tests, asupstream's
unit-tests.ymldoes. Running fork tests locally needs Bun onPATHtoo.release-javascript-benchmark.yml, a pull-request workflow; the disable scriptcan only turn it off once it is on GitHub, so it ran once on the sync PR and was cancelled.