Conversation
…on (stablyai#23597) * fix(terminal): let unselected Cmd+C reach apps that negotiated kitty keyboard On macOS Orca swallowed an unselected Cmd+C as a no-op copy, so a full-screen TUI like Codex, which captures the mouse and keeps its highlight out of xterm's selection, never received its own copy chord. - isAppOwnedCopyChord (xterm-bypass-policy) is the one rule: macOS, no xterm selection, and non-zero kitty flags from the pane's mirror. The pane's xterm bypass and the dashboard popout's key handler both use it. - A selection copy stays claimed through its repeats and release, including custom copy bindings, so kitty event reporting cannot leak them to the PTY. - Plain shells keep sending nothing; Linux and Windows are unchanged. - The e2e kitty helpers move to helpers/terminal-kitty-keyboard.ts so the shortcut spec stays under the line limit. * fix(terminal): popout copy ownership reads xterm's selection like the pane A highlight of blank cells trims to empty text but is still a selection, so the popout must not hand that Cmd+C to a kitty app while the pane withholds it. * test(terminal): keep the held-copy binding test beside the copy dispatch tests The shortcut-policy suite is at its line limit. * test(terminal): stub a blank-cell selection without widening the preview harness type * style(terminal): tighten the app-owned copy comment and read the popout selection after the early return
…n the host and pane (stablyai#23602) * test(native-chat): await the async history and journal snapshot in three tests (#23560) #22835 made history() and journalSnapshot() async; tests from #23502 and #22944 still call them synchronously, so the typecheck job is red on every PR while main pushes do not run it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(usage): show ZCode Coding Plan quota on current main (#23520) Shows the ZCode Coding Plan quota in the status bar alongside the Claude and Codex usage readouts, reading the key from the user's own ZCode config. Credentials are scoped tightly: the host must be an exact match in the allowlist, HTTPS on port 443 only, `redirect: 'error'`, and the key is checked for CR/LF before it reaches a header. The key itself is never stored or logged — account identity is an HMAC. Both JSON inputs (a user-edited config file and the remote quota response) are narrowed at runtime rather than asserted, and the request cancels an unread response body on the error path so it cannot trip the undici parser crash (orca#8695). Co-authored-by: guanbear <guanbear@users.noreply.github.com> * fix(mobile): paired clients re-derive a kept terminal after a cold restore (#23109) * fix(mobile): paired clients re-derive a kept terminal after a cold restore A renderer frame published before a cold-restored terminal's PTY registered was fenced to an empty tab list and recorded as accepted, and the renderer never resends unchanged content. When registerPty binds a surface the accepted frame fenced out, re-merge that frame so the fence reads current state. * test(mobile): drive the live desktop window through the runtime's desktop seam * test(mobile): the re-derive path never flushes the store synchronously * test(mobile): a re-derived frame must not bring back a surface the host retired after accept * fix(mobile): a re-derived frame changes membership only for the registering surface The replay re-ran the whole accepted frame, so a surface the host retired after accept (a phone close whose remote PTY is still exiting, or a closed chat tab) came back. Every other surface now keeps the host's current decision; the removal repair is extracted from the terminal retirement helper so non-terminal tabs are removed the same way. * test(mobile): a re-derived frame must not drop or disown a phone-created terminal the desktop has not published * fix(mobile): a re-derived frame does not infer renderer retirements from its older frame * revert(mobile): drop the replay of a fenced renderer frame Reverts the production parts of a88e1eaa0a, f5b99d0003 and 5af1c6d97a: the kept renderer frame, rederiveFencedRendererSurface and its registerPty call, and the mergeRendererMobileSnapshot / removeMobileSessionSnapshotTabs extractions. The fence will instead read the host's saved membership record. The test file stays and is rewritten for that mechanism. * fix(mobile): the paired-list fence admits a terminal the saved session still lists After a cold restore the in-memory mobile snapshot and PTY table start empty, so in a repo with host-authoritative terminal membership the fence dropped a restored terminal whose renderer frame arrived before its PTY registered, and paired clients never listed it. The fence now also admits a surface the host's saved workspace session still lists (tab under the worktree, leaf in its layout), and registerPty pushes the listing so pending-handle turns ready at once. A restored pane whose PTY never returns is listed as pending-handle, as in repos that are not host-authoritative. * test(mobile): keep the desktop window stub's type assertion on its SAFETY line * fix(mobile): coalesce the registration push for a listed restored terminal registerPty pushed the paired list immediately on every registration that backs a listed surface. The desktop's graph sync after a spawn already publishes the same pending-handle to ready flip on the 50 ms coalescing window, so each restored pane cost two pushes, and a restore of N panes cost N immediate full-list pushes per client. The touch now rides the same coalescing window, which still covers a registration no graph change follows. The test's "unchanged" sync dropped the graph's tab, which is itself a change, and its no-extra-push assertion ran before any coalesced push could fire; both are fixed, and a restore of two panes is asserted to push once. The fence comment no longer claims the new clause keeps pending leaves out of the graph: once the surface is listed, its leaves pass the shared predicate through that listing, as any listed surface's do. * fix(mobile): read saved membership only from the worktree's own session partition For a runtime-host workspace, emptying the owning partition re-routes session reads to a single other partition that still lists the worktree. If that older copy lists a surface a retirement just removed, a lagging renderer frame could re-admit it. The saved-membership check now reads only the partition the worktree's host names, so it never trusts a fallback copy. * test(mobile): pin the own saved partition for every workspace host kind Also correct the immediate-emit comment: only an exit bypasses the window; a registration's ready flip coalesces. * Fix terminal focus when Cmd+J wakes a workspace (#23546) * fix: retain workspace terminal focus through wake restoration * test: reset CPU throttling after wake focus assertion * fix: require terminal textarea readiness before claiming focus * test: configure React act environment for dialog regression * fix(mobile): size a terminal's first subscribe from the document's reported cell box (#23080) * fix(mobile): size a terminal's first subscribe from the document's reported cell box #22960 sent phone dims on a terminal's first subscribe by opening a throwaway empty terminal (init 80x24 ""), awaiting its ready and measuring, behind a per-document first-subscribe mark whose lifetime was tied to web-ready. That cost a second xterm/WebGL instance and ~150 ms per open, plus lifecycle state. The document now measures the cell box without a terminal (xterm 6's CharSizeService strategy, rounded as the renderer rounds it) for every text-size preset and reports it with its viewport in web-ready; a table, because the text scale only reaches the document after that notify. Each init's ready reports the box xterm actually laid out, which replaces the probe's entry. The controller answers fitDimensions/measureFitDimensions from that table and the view's layout with no message; without a table it asks the document as before. The session seeds an unmeasured viewport synchronously in subscribeToTerminal, so the first subscribe carries dims by construction. Deleted: the empty init, its awaitReady gate, deferFirstSubscribeUntilViewportMeasured and the subscribedDocuments mark. The fit pass is unchanged and still covers a document that reports no cell box. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): read the reported cell box through in-narrowing, not Reflect.get Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): correct the probe's cell-box guess from the box xterm lays out The web-ready probe is a guess: building the WebGL addon creates no context, so a context that fails on load lands on the DOM renderer, whose width is not snapped and depends on the column count. Before, a ready box that differed was only logged; the first subscribe had carried the wrong column count, the host echoed it, the fit pass saw the viewport equal to the host's dims, and the grid stayed slightly shrunk. The store also kept the WebGL width after a context loss. The document now reports the box xterm laid out whenever it changes (from onRender, which covers a renderer swap and a DPR change that onDimensionsChange does not fire for, and at ready). The store replaces the guess; when that changes the current text size's entry, the view calls onCellBoxChange with xterm's grid and the session re-fits, running the bounded fit pass if the dims moved (one resubscribe). Equal boxes do nothing. The RN layout box now survives a document reload; the document's own viewport only stands in until the view reports a layout (on the page, web-ready arrives first). The mismatch console.log is gone, and the probe's rounding names the xterm version it copies. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): correct each cell-box guess at most once, so a DOM renderer cannot loop On the DOM renderer the cell width is the rounded canvas width divided by the column count, so every re-init at new cols reported a new box. Each one counted as a correction, a floor over floats could flip the fit between two sizes, and each flip landed converged, which reset the resubscribe budget: an unbounded series of full-snapshot resubscribes. Only the first laid-out box for a guessed text size may be a correction; later reports still update the store, so fits stay truthful, but never resubscribe on their own. The fit's floor gains a 1e-6 epsilon so floating-point error at an exact boundary cannot flip a column or row. New document tests pin the render report after a renderer swap and the report at ready for a paused renderer. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): make xterm the only terminal cell measurer The document builds its real terminal before web-ready, at the app's text scale, and reports the box xterm laid out; the first init reuses that terminal. The page-side prediction, the per-scale guess table and the once-per-document correction are gone. The app remembers the box per text scale for its lifetime, so a later open at a known scale subscribes with phone dims at once. A box that changes at the same grid (renderer swap, pixel ratio) refits the open terminal in place. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep commands queued before the terminal WebView first loads A subscribe sized from the stored cell box can queue init before the native WebView reports its first load start, which cleared the queue and left the terminal blank. Only a reload now drops queued commands. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): re-init a document that lacks the subscription's init, and fit one frame width - Web-ready now says whether the document holds the terminal's latest init (a reload before the first ready drops a queued one); the session resubscribes any initialized terminal whose document lacks it. - One grid fit, shared by the app and the document, fed the unrounded frame width React Native laid out; it keeps exact fits whole at fractional pixel ratios. The document's viewport-width fits are gone. - The page builds every document at the scale the view mounted with, as the native WebView does. - A new document's first cell box is compared against the grid the subscribe fitted from the stored box. - The terminal built before ready stays hidden until its first init. - The cell-box census matches glyph-measurement techniques, not names; the store's unused clear() is gone. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): build the terminal before ready only for the view shown at mount A session mounts one terminal view per tab, and each built xterm and a WebGL context before ready: 20 tabs made 20 contexts at load, past the ~16 a page (or Android's shared WebView renderer) holds, and native logged 32 context losses. Only the view shown when it mounts builds early now; the rest build at their first init as before. Deferring the WebGL addon instead would change the reported box: the DOM renderer lays out 7.8x15 where WebGL lays out 7.667x15 at the same font. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write a WebView document's start values into its page, not an injected script Android ran the pre-content injected script after the document's own in 1 of 22 documents on the emulator; that document started with no text scale or shown flag and built a terminal it should not have. The values now sit in the page ahead of the document script, one source object per start pair so a render never reloads the WebView. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): the pre-ready terminal measures and reports while hidden Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): measure only the laid-out frame, and refit on a new grid, not a new width - A measure needs both of the frame's dimensions from React Native; the document's viewport-height fallback is gone, and before the first layout the handle answers no fit without asking the document. - A frame width change that still fits the PTY's grid from the stored box is a no-op, so sub-pixel layout jitter no longer re-measures. The width ref is written in that effect rather than during render (react-doctor). - One "last grid" ref: the last reported grid, or the one a subscribe fitted from the stored box. - The page render rig measures through the frame it laid out, as the session does, and lets the replay's fit settle before its resize-refit witness. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let only the current terminal document's ready flush A reload kept the WebView and its onMessage, so the old document's late web-ready flushed the queue into the reloading view and the new document got a second init. Each document now gets its own view (keyed on a generation the controller owns), every notify carries the generation of the view that received it, and a web-ready from a replaced document flushes nothing and stamps nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop every notify from a replaced terminal document One rule at the receive boundary: a notify from any generation but the current one is dropped, whatever its type, not only web-ready. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make fitDimensions a pure question; name each generation counter - fitDimensions no longer records the grid. A width change to a new grid asked it first, so the DOM renderer's report of that grid's box read as "same grid, new box" and refit again. Only the first-subscribe seed (seedFitDimensions) records the grid the document's first report is checked against. - viewGeneration counts the views, readyGeneration counts web-readies. - replaceDocument no longer resets the load flag; the load-start reset stays as the guard for a view that reloads itself. - The name-based lifecycle census is replaced by a behavioural test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): typecheck the handle mocks, drop the unused cell-box get - The two handle mocks carry both fitDimensions and seedFitDimensions, and the fake-timer acts return nothing, so the three test files check under tsconfig.test.json again. - terminalCellBoxes.get had no product caller; the store's tests assert through fit. - The load-start comment says what the controller does now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hold the grid the document has, ignore a replaced view's load start, dispose a failed pre-ready terminal - The document reports a new grid even with an unchanged box, so an in-place reflow on WebGL is held before a later renderer swap at that grid; the swap then refits. The app's apply paths do not hold the grid themselves: the DOM renderer's box follows cols, and a grid held on apply would read its own box as a renderer change and loop. One writer (holdGrid) holds the seeded or reported grid. - A load start from a view a replacement unmounted is ignored, as its notifies already are. - A terminal whose open throws before ready is disposed, not only unreferenced. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): ignore every native event from a replaced terminal view One wrapper binds each WebView lifecycle event (load start, error, HTTP error, render process gone, content process terminated) to the view's generation, so a replaced view's late event cannot reset, replace or put an error over the current document. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): a DOM seed refits once on its first report, not on the refit's own Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): subscribe a terminal only after its document is ready The document still builds its terminal before ready and reports the cell box xterm laid out in web-ready; the app now subscribes after that ready and fits from that box, so nothing is sent to a document before it is ready. Everything that made a pre-ready subscribe safe goes: the app-lifetime box store, the seed fit, the per-document view generations and their event filtering, the init tracker and the hasInit resubscribe. The native view reloads in place again and web-ready keeps main's reload rule. Boxes are kept per view; the grid a document last reported still guards the in-place refit against the DOM renderer's cols-dependent box. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): hold one reported cell box and the grid the subscribe fitted The controller keeps only the box the current document last reported, not a per-text-size store: the document re-reports on a scale change. The subscribe after ready fits from that box and holds the grid it fitted, so the DOM renderer's first report at that grid (a new box) refits once in place and converges; refit and apply paths hold nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): fit only a ready box at the app's scale; forget a reloaded document's box and grid A reload keeps the document's mount scale, so a ready after a text-size change reports a box at the old scale; that box no longer sizes the first subscribe, which then takes the no-box path. A readiness reset drops the old document's box and held grid, so the new document's first DOM report at the same grid does not refit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give the terminal document its frame at init, and fit text scale over it only A subscribe sized from the ready box sends no measure, so the document had no frame when the text size changed and reported the pre-refit row pitch. The app's init now carries the frame it laid out, in the fields a measure uses; the router takes it from either. The text-scale fit reads only that frame, with no viewport fallback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say why a frameless text-scale change skips the resize Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): one cell box per terminal notify, not an array web-ready and cell-metrics carry `cellBox: {fontScale, cellWidth, cellHeight} | null`; the document's `laidOutCellBox` returns one or null and the parser validates one object. The text-scale match moves from web-ready into `handle.fitDimensions`, the one place a box is fitted. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): fit terminals in the app from the reported box; drop the measure round trip The app already holds the box the document reported, so the refit and the fit pass await the init's ready and call `handle.fitDimensions` instead of posting `measure` and waiting on `measure-result`. The document's measure, its retries, and the measure promise and timeout go. The document still resizes locally on a text-size change, so every grid the app sends (init, resize, reflow) carries the laid-out frame it was fitted to. `holdSubscribedGrid` replaces `subscribeFitDimensions`, so the only fits are `fitDimensionsFromCell` and `handle.fitDimensions`. The render rig reads its fit from the ready box. The recorder adapter mounts the new handle with the same recorded effects; the goldens it mounts move on their adapterSha256 header only. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): keep the terminal frame in one ref, and notify a new width imperatively The session held the frame in a height ref, a width ref, a width state and the refit's own width ref. It now holds one `terminalFrameRef` ({width, height} | null until the first layout; a hidden 0x0 layout keeps the last box). onLayout notifies a new width imperatively, as it does height, and the refit's notify skips a width whose fit is the grid the PTY has. `terminal-frame-width-refit.ts`, the width state and its effect go. The subscribe's layout gate reads "no frame yet" directly. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): subscribe a held-back terminal on the frame's first layout only `handleTerminalFrameLayout` ran on every onLayout; it now runs once, when the frame first has a size. Later layouts only notify a new width. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): size the first subscribe inline in subscribeToTerminal `sizeTerminalViewportFromCellBox` wrapped five lines in a 37-line module; the subscribe now fits the ready box against the frame, holds that grid and records the diagnostic itself. The helper's tests fold into the subscription tests, which move to the subscription's name. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the unreachable font-size guard on the reported cell box xterm 6.1.0-beta.303 updates the render service's cell box in the same task that sets `options.fontSize`: CharSizeService.measure fires onCharSizeChange, and RenderService.handleCharSizeChanged runs the renderer's `_updateDimensions` (DomRenderer.ts:359, WebglRenderer.ts:229). `term.onRender` fires from RenderService._renderRows after the rows are drawn (RenderService.ts:213, CoreBrowserTerminal.ts:538), and the document writes its text scale and the font size in one task (text-scaling.ts applyTextScale, terminal-init.ts init). So no report can read a box between the font and the scale; the guard and its test go. A new test pins the real order: no report when the font is set, the new box at the new scale on the next render. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): one start seam, no source cache, the reported box as an object - `useState` already pins each view's WebView source at mount (a new test re-renders at another text scale and gets the same object), so the module-level `webViewSources` Map goes. - `initialTextScale` and `buildsTerminalBeforeReady` become one `start(): { textScale, shown }` seam. - `reportedCellBox` holds the last reported box and grid, not a string key. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the RPC recordings to this branch and re-record The terminal refit now fits in the app from the reported box and reads one frame ref, so the recorder's terminal adapter mounts the new handle (`awaitReady` + `fitDimensions`) and options (`terminalFrameRef`), keeping its recorded effects. `baseline` is repinned to 21954dbd2f1, the last commit to touch a fenced path, and every golden is re-recorded. Proof by class against HEAD: 787 header-only, 0 body moved, 0 added, 0 deleted. Header keys moved: `baseline` on all 787, and `adapterSha256` on the 14 goldens `terminal-mount-adapters.ts` mounts (query-reply 3, accessory-raw-send 4, takeover-report 4, viewport-refit 3). No recorded traffic moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): hold the reported cell box and its grid in one ref The controller kept the box in `cellBoxRef`, the grid in a string `lastGridRef` and wrote it through `terminal-held-grid.ts`. One `heldRef` now holds `{ cellBox, grid }`, as the document's own `reportedCellBox` does: web-ready writes the box, every cell-metrics report writes both, `holdSubscribedGrid` writes the grid, and a readiness reset clears it. Same write points, so the one-refit bound holds; the DOM-loop and refit-once tests pass unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the RPC recordings to the hold-rule commit and re-record H (f00bebba483) touched a fenced path after the last repin, so `baseline` moves to it and every golden is re-recorded. Against the corpus before this branch's refreshes (21954dbd2f1): 787 header-only, 0 body moved, 0 added, 0 deleted; `baseline` on all 787 and `adapterSha256` on the 14 goldens `terminal-mount-adapters.ts` mounts. Against the previous refresh: `baseline` only. No recorded traffic moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the RPC recordings to the main merge and re-record The merge (b3b1b0def22) is the last commit to touch a fenced path, so `baseline` moves to it and every golden is re-recorded. Against 97b5bb2b9ac: 787 header-only, 0 body moved, 0 added, 0 deleted; `baseline` on all 787, and `adapterSha256` on the 14 session.diff-review-actions goldens whose adapter #22951 edited. Against origin/main: 787 header-only, 0 body moved/added/deleted; `baseline` on all 787 and `adapterSha256` on this branch's 14 terminal goldens. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): the terminal document holds the grid and decides each refit The document already kept the last reported box and grid; the app kept a mirror of both to decide the refit. Now the document decides: its `cell-box` notify carries `{ cellBox, refit }`, sent only when the box changes, with `refit` a box that changed at a kept grid. web-ready records the pre-ready terminal's box at its 80x24 grid, and the first init that reuses that terminal holds the init's grid, so the DOM renderer's first report refits once, as the subscribe's hold did. A re-init no longer clears the record, so a new renderer at the same grid still refits. The app keeps one `cellBoxRef` and `holdSubscribedGrid`, `heldRef` and the grid on the notify go. The one-refit, DOM-loop and renderer-swap tests move to the document with the same scenarios. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): one init options object, and a frame on every grid `init` takes `{ cols, rows, data, preserveScroll, oscLinks, frame }` instead of six positionals, and `init`, `resize` and `reflow` (handle and messages) require `frame: TerminalFrame | null`. The refit's reflow check reads `!dims` alone, and the controller's test file is named for the `cell-box` notify it now covers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): one notifyTerminalFrame for the frame's layout The frame's onLayout made four calls and held the classification itself. It now calls `notifyTerminalFrame({ width, height })`, and the session's terminal-webview hook keeps the one frame ref, notifies the height, subscribes the document held back for the first layout, and notifies a later width change. `handleTerminalFrameLayout` is named for what it does: `subscribeIntendedActiveTerminal`. The layout tests move to that hook. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the RPC recordings to the round-8 head and re-record 85d421963c6 is the last commit to touch a fenced path. Against a676c1b65a7: 787 header-only, 0 body moved/added/deleted, `baseline` only. Against origin/main: 787 header-only, 0 body moved/added/deleted; `baseline` on all 787 and `adapterSha256` on this branch's 14 terminal goldens. No recorded traffic moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): name the init option initialData, as the message does The init option `data` becomes `initialData`, the message field's name, so the controller passes it through unrenamed. The `preserveScroll` why stays on the message type only, and the document test's title names the three grids that carry the frame. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the RPC recordings to the round-9 head and re-record 486566c82b4 is the last commit to touch a fenced path. Against 3371c397150: 787 header-only, 0 body moved/added/deleted, `baseline` only. Against origin/main: 787 header-only, 0 body moved/added/deleted; `baseline` on all 787 and `adapterSha256` on this branch's 14 terminal goldens. No recorded traffic moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * ci: rerun checks against main with #23560 landed Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(ai-vault): show ZCode CLI session history (#23513) Surfaces ZCode CLI session history in AI Vault, so past ZCode sessions show up next to the other agents' instead of being invisible. ZCode stores sessions in the same SQLite shape OpenCode uses, so this reuses the existing OpenCode lister and parser rather than adding a second scanner — the worker only varies the agent it stamps on each row. Discovery covers the native home and any WSL homes. SQLite rows are narrowed at runtime rather than asserted: the statement API returns untyped column values, so the declared row shape is only a claim until something checks it, and a drifted schema or a database written by another tool reaches the same code. Co-authored-by: guanbear <guanbear@users.noreply.github.com> * test(mobile): repin the RPC recording corpus to main after #23080 (#23565) #23080 squash-merged a corpus pinned to its branch commit 486566c82b4, which the squash left unreachable from main. Repin baseline to main's tip and re-record; every golden moves only its baseline header. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(ci): diff against the merge commit's first parent so PR checkouts can be shallow (#23562) Every changed-path gate asked git for `--merge-base "$BASE_SHA" "$HEAD_SHA"`, which needs the event payload's base SHA to be in the local graph. That is the only reason two jobs cloned all 8127 refs' history. On a pull_request checkout HEAD is already the merge commit, so its first parent is the base side and no merge base has to be computed. config/scripts/git-pull-request-diff-base.mjs resolved that for the two Node gates; the workflow's inline gates now use the same helper through a small CLI rather than open-coding it. code_paths gates all 22 jobs, so its checkout is charged to the start of every one of them: measured 20.7s to 1.6s, keeping blob:none because its sparse tree is ~7 files and leaves no blobs to refetch. Static analysis drops the filter instead, since populating all 30,226 files makes blob:none force a second promisor fetch: 23s to ~11s. Verified on a real merge ref. At depth 50 the old and new forms produce identical changed-file sets. At depth 2 the new form still works and the old one fails with `fatal: bad object`, which is the failure a stale base would have caused once the checkout stopped being complete. Also drops the dead resolveBase + merge-base prelude in the changed-code gate, whose result resolvePullRequestDiffBase already discarded on every PR. * feat(mobile): the keyboard covers the page like a native screen, and the shell says its height (#23110) The shell no longer shortens the WebView for the keyboard; it publishes the keyboard height like the safe-area insets, so native's keyboard lift, refit hold and dismiss key run on the page unchanged. Keyboard and inset arithmetic read the shell's OS through a host-os seam. One page-version floor (manifest pageVersion, shell floor 1) replaces per-feature accept negotiation; a page below the floor gets the existing update wall, a desktop with no bundle keeps native screens. iOS shell drops the form accessory bar and its own keyboard observers. Native session screens untouched. * fix(agent-session): wait for in-flight session-store writes before teardown returns (#23545) * fix(agent-session): stop lease renewal before the renewal's write lands Clearing the renewal interval only cancelled the next tick. A tick already past its guard still had a whole-file store transaction to commit, and the store's transaction lock re-creates the store directory before it writes, so that commit could land after host teardown had finished releasing everything it touches. `stop()` now resolves once the tick in flight has finished writing, and host teardown's stop-lease-renewal phase waits for it. The three test harnesses that model a host vanishing without a clean quit shared a copy of the same incomplete shutdown; they now share one helper that waits. The symptom was a CI flake: the refusal-oracle spec removes its temp directory in `afterEach`, and a renewal landing mid-removal put the store directory back, so the removal failed with ENOTEMPTY on the temp root. * fix(agent-session): wait for the delivery loop's restart when abandoning a host The abandon helper disposed the delivery loop and moved on. Disposing only stops the loop's NEXT step: a step already past that check keeps going, and the restart it runs for an accepted send reserves an owner, which is a store commit. The store re-creates its own directory before every commit, so that commit put the directory back under the temp-directory removal the test does next, and the removal failed with ENOTEMPTY. Quit already waits for exactly this work, in its drain-attaches phase — every attach is registered with the task queue from enqueue. The helper now runs the same drain, in quit's order, so it waits for both producers that reach the store after the last awaited call returns. Adds a regression test that holds the loop's restart inside its provider acquisition and asserts abandoning does not return until it lands. * chore: re-trigger PR checks The push to 2ecc9c6e emitted no pull_request event, so the matrix never ran. * fix(sidebar): clip worktree card content to its border (#23566) * fix(runtime): answer terminal.subscribe at once for a pane the desktop already has mounted (#23512) * fix(runtime): answer terminal.subscribe at once for a pane the desktop already has mounted A mobile subscribe to a PTY with no headless model asked the renderer to mount its tab and waited for a newer serializer settle. The renderer drops mount requests for tabs it already has mounted, so a reattached daemon PTY whose restored provider snapshot outranked the live renderer held the reply for the full 3 s deadline. A live renderer screen now proves attachment and is adopted directly; an unmounted pane still requests the mount and waits. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): treat any renderer answer, even a blank screen, as an attached pane A fresh shell that has printed nothing has a registered serializer and an empty screen; requiring non-empty data sent it back through the dropped mount request and the 3 s wait. A blank screen skips the wait but does not replace the chosen snapshot, so a parked pane cannot erase provider history. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): treat a mounted pane with unsettled output as attached The stable renderer snapshot returned null both when no renderer answered and when output advanced under every retry, so a desktop pane printing continuously still took the dropped mount request and the 3 s wait. It now returns a typed outcome (settled, moving, absent); moving skips the mount and publishes the chosen snapshot, and late recovery still requires settled. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): adopt only a renderer-ordered screen when probing a mounted pane The attachment probe read the terminal before knowing it would adopt, which can reach the provider snapshot on the unmounted path; the read now follows the decision. A seq-less renderer screen would replay every buffered chunk on top of itself, so the probe keeps the chosen snapshot for it. The probe, adopt and mount wait move into their own module to stay under the line cap. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): adopt a seq-less renderer screen when no output is pending The seq gate only prevents a double replay of buffered output, so a settled non-blank screen without a seq is safe when nothing is pending. That keeps the better screen for a pane right after a deferred cold restore, before it is renderer-ordered. The rule now applies after the mount wait as well. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(runtime): decide renderer attachment from the host's serializer flag The mounted-pane probe re-derived attachment by serializing the renderer up to six times, which cost ~7.5 s for a registered but unresponsive renderer on a busy PTY. The host already holds that fact in the serializer readiness flag. The flag is never cleared when a pane closes over a live PTY, so one null serializer answer falls back to the mount wait: worst case is the old 3 s plus one 750 ms serialize. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(runtime): let the serializer's answer alone prove renderer attachment serializeRendererTerminalBuffer already answers null when the host's serializer flag is unset, so the separate flag accessor was redundant. The numeric-seq adopt test now replays only the byte past the seam. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * Revert "perf(ci): shard the anti-slop audit across processes instead of one JS runtime (#23543)" (#23575) This reverts commit fae0ae7a464c2991bd25885e7e61689c7f471a0f. * perf(ci): cache pnpm verification records on Linux (#23568) * perf(ci): pilot pnpm verification record caching on Linux * test(ci): review pnpm verification record in mobile cache audit * fix(native-chat): the host writes chat failures for a person, with a typed fact beside them (#23116) * refactor(native-chat): remove the unused terminal handoff No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths. * fix(native-chat): never let the pre-stop snapshot hold a chat's stop Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop helpers only the terminal handoff called `claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and `queryWindowsProcessRowsFresh` lost their last caller with the handoff. The fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`, the teardown path that still depends on that contract. Co-Authored-By: Claude <noreply@anthropic.com> * docs(native-chat): stop citing the removed handoff in lifecycle comments Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stalled snapshot drain without a cast Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): pin that a start dead before proving owes no settlement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(terminal): remove the agent-session PTY write gate The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(native-chat): drop the transcript helpers only the handoff called appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com> * fix(native-chat): stop calling a starting chat "mid-handoff" A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * test(native-chat): type the stand-in roster decoder without a cast Co-Authored-By: Claude <noreply@anthropic.com> * refactor(codex): name the pinned rollout lookup for what it does With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow. * refactor(native-chat): type the owner-status reply as the host sends it The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged. * refactor(native-chat): normalize terminal-handoff lease values once at decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind. * refactor(native-chat): stop threading the owner kind through a reservation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it. * test(native-chat): pin the legacy-lease rewrite with a transaction that changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write. * fix(native-chat): name a chat write by its target, not the owner generation A write carried the fence of the last frame the pane read, and the host refused it unless that fence was still current. An idle release and the restart after it each move the fence, and the release publishes nothing, so a send after a release was refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a cold start was refused as stale. Every write already names what it acts on: a send its conversation, a cancel its turn, a prompt answer its item revision, a rewind its epoch; an option is last-writer-wins. So admission stops comparing the client's fence, and the rebase that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it. The writer-lease check stays, and so does the attach's compare-and-swap. Frames now stamp the fence read when each frame is sent instead of a copy each subscriber kept, which went stale on the same release. * fix(native-chat): every journal append reaches the chats that are open A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted. * test(native-chat): an epoch replacement reaches the open chat * test(native-chat): each row reaches an open chat once, and a live handle enters only through the map * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. * perf(native-chat): a publish behind a delivered commit reads nothing Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read. * test(native-chat): state why the teardown test's fake journal is safe to cast * docs(native-chat): say mutation admission checks only the writer lease * docs(native-chat): drop the send rebase from comments that still described it * fix(native-chat): a message is accepted, then delivered A send to a chat with no running agent restarted the agent inside the send call, before the message was recorded, so the client waited for the whole start and a failed restart refused the message. Claude held prompts sent during startup, and those could settle as "unconfirmed". A send is now accepted inside the session's serialized queue: one ledger row and one submission row marked handoverRecorded, published, answered pending. A per-session delivery loop exists while a message is queued. It starts the agent through the same serialized attach a hold uses, waits outside the queue for a Claude child to prove its start, and hands the oldest queued message over as its own serialized step, writing dispatch{pending} before the adapter call. A start it needed and did not get writes one error-tone row and rejects every queued message with the same words; a start Stop cancelled writes none. Settlement follows from the rows. A queued message is provably unwritten, so a close, an eviction or an exit rejects it. A handed-over message stays in doubt. A queued row at or below the sequence a handle found when it opened was left by an earlier process and is rejected at open, with no latch. Stop withdraws queued messages with no writer lease and no fence. An attach failure keeps the conversation open, and the attach adopts its journal. Owed work counts the loop and queued rows. A compaction or rewind found prepared when a conversation opens was started under a child this process no longer has, so the open settles it rather than leaving it to refuse every send until a view attaches. The open cursor is scoped to its epoch, because sequences restart when an epoch is replaced. Deleted: restart-before-admission, recordFailedRestart, the fence rebase, Claude's startup gate, the attach's forget on failure and its own crash boundary. Clients without agent-session.accepted-send.v1 get their reply held until the handover; the desktop and paired desktop lists advertise it. * fix(native-chat): settle queued messages only for the child that ended A child that proved its start and then exited before its message was handed over left the message queued: the exit settlement returned early when nothing else was in flight. Delivery then started another child for it, and a child that died the same way started another, without end and without a row. A retried settlement for an earlier generation, run by the attach that delivery started, did the opposite: with that generation's turn unfinished it rejected the message queued for the child being attached. The settlement now takes the rejection for queued messages from its caller. The unexpected exit and the eviction pass one, and it applies even with no other work in flight; the retry for an earlier generation passes none. * fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. * perf(native-chat): the recovering open reads the journal once Every conversation open now goes through the recovering open, including the read restore of every chat at startup, which used to replay its journal once. The recovering open replayed it twice: once to probe it and again inside the open. The probe is now handed to the open as its load. * fix(native-chat): an attach that fails after indexing its child leaves no child behind A failed attach now keeps the conversation open, but a failure after `onAttached` indexed the child (the rewind or compaction recovery, or the attach's own success record) left that entry claiming a child the failure path had already released. The next send found the phantom, skipped the start, and wrote at a fence the journal had moved past, so the message stayed queued for good. The entry now drops the released child and its event sink, and follows the record's fence, as a failure before indexing already did. * fix(native-chat): a withdrawn message shows no error, and a rejection outlasts the send's answer The error strip for a message the host accepted and then did not deliver matched the entry before the outbox reconciled, so a Stop's withdrawal, which the reconcile drops, showed "Orca could not send your message" with nothing to retry. It now reads the reconciled entry. A rejection the journal records before the send's own pending answer lands is final as well: that answer no longer puts the entry back to dispatching with no Retry. * fix(orchestration): a structured worker whose agent outlasts the preamble wait is left unknown, not torn down The preamble waits for its submission to be delivered while the worker's agent starts. When that wait ran out it threw operation_unknown, and the failed-start teardown then closed the session, which rejected the very preamble the host was about to deliver. It now reports a turn start nobody observed yet: the worker is start-unknown with its session kept, the host delivers the preamble when the agent starts, and the worker's report settles the dispatch as for any unobserved start. The receipt no longer suggests reading a screen a structured worker lacks. * fix(native-chat): a message rejected while its chat was closed reads as not sent A remount reads an entry it left dispatching as unconfirmed. When the journal had rejected it meanwhile, as a failed start or a quit now does, the reconcile left it unconfirmed: it blocked every later message behind a Retry and no reason, and the delivery probe, seeing the journal already answered, never ran. The reconcile now settles it as rejected like a dispatching one. * test(orchestration): name why the readiness settlement fakes are cast * fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent * docs(native-chat): drop the fence from the admission the send effects run behind * docs(native-chat): give the fence move on release the reason that still holds * docs(native-chat): stop citing a write fence check in launch and mailbox comments Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease. * refactor(native-chat): the provider child is its own record A conversation now outlives any number of provider children, so the child is one record on the conversation's entry instead of five loose fields beside its journal. It is written in one place: indexed only once an attach has fully succeeded, and ended through one function that an exit, a failed re-attach, a Stop and an eviction all share, matched on the child's generation and fence. - A failed attach writes no child, so there is nothing to unwind: the field unwind and the fence patch after it are gone. - Conversation writes read the record's fence, the way mutation admission already does; a child's own writes use its fence. The four stored-fence patches, and the settlement retry's overwrite of the conversation's fence, are gone. - The owed wind-down is its own tombstone, carrying the child it is owed for, and is no longer dropped when an attach replaced the whole entry. - Stop on a child still proving its start stops only the child: its lease goes back and the chat is told it is idle, but the journal, the holders and the readers stay. Close is that stop plus the conversation's close. - The settlement retry uses the conversation's own journal, opened through the host's one open. * fix(native-chat): the delivery loop alone settles a message its start or child failed A queued message was settled by whichever path happened to end the child first: the loop, the unexpected exit, eviction's work settlement, the open's leftover rule, and the startup branch that rejected every pending row. That gave two failure rows with different tones for one start, a loop that could hand over to a different child than the one it waited on, and a Claude start that died while starting reading unlike every other failed start. - The loop remembers the child it waited on. At handover, if that child is gone or replaced, it reads how it ended: a Stop continues; anything else writes one failure row and rejects every queued message with the same words, then stops. A child still starting whose start the adapter says did not land fails the same way. The exit, eviction and the settlement retry only settle the handed-over and legacy rows of the child that ended. - One failure row, always an error, keyed by the start. A start a view began that dies with nothing queued writes the same row through the same builder, so a second report revises it. - The open no longer rejects leftovers; the loop's first step does, and the open wakes it. - `awaitStarted` answers why a start did not land, so the row says it even when the loop sees the failure before the exit is processed. - Quit closes every conversation the way closing a chat does: what is still queued is rejected as closed, with or without a child, and a start the loop already has in flight is waited for so the child it produces is stopped rather than left behind. * refactor(native-chat): a stopped child ends on the one reading of its stop The eviction step reads a stop's result through `stopAgentSessionProviderRoot` and hands that verdict to the child's ending, so the host never forms a second view of whether the root is gone. Every ending carries it: a stop's comes from that reading, an exit's root is gone by definition, and a failed re-attach passes what its release saw. The end-of-child record can therefore also carry a stop whose root was not seen to go, which nothing ends on yet. * feat(native-chat): the host says it accepts a send before any agent has it The host now lists agent-session.accepted-send.v1 among its own runtime capabilities, the same string capable clients already send. A client can then tell a host that answers a send at acceptance, and admits a Stop with no writer before a turn starts, from an older one that still restarts the agent inside the send. Additive: an older client ignores a capability it does not know. * refactor(native-chat): an attach never opens a journal of its own The attach adopts the conversation's open journal, which outlives it, so it no longer opens one for a direct caller either. That leaves nothing for a failed adopted import to close, and the flag that told the two cases apart is gone. Tests that attach without a host open the conversation the way a host does. * fix(native-chat): a moved fence resends nothing on a host that accepts first The outbox treated any fence change as a new owner: it dropped the answer of a send in flight, queued that send to go out again under the same id, and unblocked a refused head. On an older host that is how a send the restart refused, unrecorded, gets another try. On a host that records every send before it starts an agent, a fence moves because that start ran, so the same rule resent into every failed start. With a fence stamped on every frame, that became a loop. The outbox now reacts to a fence change only when the host has not advertised that it accepts a send before any agent has it. On such a host, only a Retry or a new send goes out, and a failed start reaches the client as a rejected message it keeps with its Retry. Against an older host, or before one has answered, the outbox behaves as it did. Desktop and paired web share this hook. * refactor(native-chat): a child's end says whether the user or the host stopped it The end-of-child record's cause now tells a user's Stop from the host stopping the child for a cause of its own: `user-stop` and `host-stop` replace `stop`. The delivery loop goes on after a user's Stop, as before, and fails the start it was waiting on after a host stop, with the one error row and every queued message rejected, in the stop's reason when it gave one. The reason stays description only. Stop passes `user-stop`; nothing passes `host-stop` yet. * fix(native-chat): a chat whose only work is a queued message is not offered for resume A message accepted while the agent was starting counts as working in the chat, and quit rejects it as never sent. The teardown snapshot read the same working rule, so a relaunch offered to resume a chat whose agent never had the message. The snapshot now reads only what was handed over. * test(native-chat): type the queued-message fixtures in the resume-offer tests * fix(native-chat): a start that dies while a message waits on it is that message's failed start Opening a chat's tab starts an agent for the view, and a send accepted meanwhile waits on it. When that start died, its exit wrote the start's error row and left the message queued, so the delivery loop started a second agent into the same failure and wrote a second row. A child's end now records where the conversation's journal stood, and the loop settles a message accepted before a failed start ended with that start: one row, under its key, and no second start. A message sent after the failure still gets a fresh start. * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): pin what a failed start settles, and what a resume offer names A view's child that dies while a sent message waits settles that message only when it died starting and no child has taken its place: a proven child's crash, or a second start since, gets the message delivered. The resume offer names the handed-over message, never a newer one still queued. * test(native-chat): the failed-start pins fail on what the message became, not on a timeout * test(orchestration): the preamble's host stub is typed, not cast The preamble send now takes only what it reads of the host, the send, the settlement wait and the record's fence, so its test builds that host with real types instead of `as never`. * feat(native-chat): a typed failure fact beside every failure sentence Adds the shared vocabulary the host writes a failure with: a closed failure kind, a provider diagnostic that says who it is for (a person, or a log), and a refusal cause beside the refusal code. Status rows gain an optional failure fact and rejected submissions an optional rejection fact; the dispatch row carries it, the reducer reads it field by field, and the projection forwards it. Older rows and older readers are untouched: every field is optional and the schemas stay open. * fix(native-chat): durable failure rows and rejection reasons are written for a person Every host writer that records a failure now writes a sentence for a person beside a typed fact, instead of embedding a refusal's message, an exception or a composed exit string. A provider's own words travel as a separate diagnostic from the places Orca composes them - the Claude and Codex exit stderr (a log), Codex's JSON-RPC message, Claude's compact_error and Codex's turn error (for a person) - and are never inferred from a string afterwards. Not signed in and oversized history are typed at the adapter that detects them. Covers start and restart failures, the delivery loop, dispatch rejections (content, queue-full, write failures, provider refusals), cancel and answer confirmation rows, compaction, the rewind fallback, and not_delivered, which released clients printed as it was. Two leaks close on the way: a settlement retry no longer writes Orca's probe evidence into the exit row, and an attach or journal-sink failure is recorded as Orca's fault rather than as the provider stopping. The legacy rejection markers and the reasons on sends in doubt stay byte-identical. * feat(native-chat): refusals name their cause, and a failed start is worded in one place A refusal now carries an optional cause beside its code: one closed enum of the situations a chat write can meet, set at every emitter a structured-chat write reaches. Returned refusals build it with refuse(code, cause, message). Store and host paths that raised a bare Error(code) now throw AgentSessionRefusalError, whose message is still the code and which has no code property; the RPC error mapper handles it before any other passthrough, keeps today's wire code and message byte-identical, and adds { refusal: { code, cause } } to the error's data. The hold throws it, and restart-resume files the cause beside the unchanged reason. The operation ledger stores the cause beside the code, so a replay names the same situation as the first answer. The sto…
…blyai#23677) * test(claude): expect typed cancellation in queued-send settlements * fix(ci): respect disabled terminal links and await browser recovery * test(e2e): give legacy close client a profile authority
stablyai#23689) On narrow windows the right-side status labels (update ready, memory, keep awake) went icon-only while every agent stayed in full, so the +N badge never appeared until much narrower. Add a density level that collapses calm agents into +N while the right side keeps its labels; it fits by the width with urgent agents pinned, and only when even that doesn't fit do the segments go icon-only.
…stablyai#23676) * fix(mobile): truncate oversize markdown reads instead of failing them The desktop bridge refused markdown over a private 512 KiB cap with file_too_large, which reached the phone as a generic runtime_error that the reader discarded, so a 632 KB file showed "Couldn't load markdown". Reads now return a UTF-8-boundary prefix under one shared 2 MiB budget, marked truncated with the full byteLength and read-only. The phone shows the truncation like file tabs do and maps refusal codes to real copy, so an older desktop's refusal reads "File too large for mobile preview". Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin that a truncated markdown read is never editable The read budget sits above the edit budget, so every truncated document is already read-only as file_too_large. Pin that ordering so a future budget change cannot make a prefix editable, and name the constant as the markdown preview budget, separate from the file preview's own. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refuse saves from a truncated markdown read A phone holding a truncated prefix must not write it back; the 256 KiB save guard refuses it as file_too_large before any version check. The shared budget test shrinks to the ordering it pins and names the bridge test as the behavioural pin. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): tighten markdown truncation types and measure once The read truncation measures the document once. The disk fallback drops its truncated-only read-only text, which the status line never showed, and both truncation fields are optional there. A markdown doc's flag is only ever true, and the schema comment names the hook, not line numbers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): drop a stale disk-fallback comment Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore: retrigger CI after stablyai#23675 landed on main Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…tablyai#23685) * ci: benchmark per-job Node compile caching on full unit shards * ci: measure unit shards with three and four workers * ci: benchmark localization extraction CLI patch * perf(build): reuse identical relay bundles across platforms * ci: compare Vitest 4 and 5 on complete ARM shards * perf(ci): upgrade localization extraction to skip irrelevant syntax walks * perf(ci): use all four ARM cores and remove benchmark workflows * ci: preserve failures while capturing unit source revision * fix(ci): preserve commented and escaped localization calls * ci: remove corrected localization benchmark harness
* fix(sidebar): move agent child disclosure to the timestamp slot * fix(sidebar): align agent disclosure with summary caret
…e shell (stablyai#22762) * fix(mobile): start AI-button agents through agent.launch, never a bare shell "Fix checks with AI", "Resolve conflicts with AI", commit-failure recovery and diff review's "New Agent Session" created a terminal with no agent and typed the multi-line prompt into the shell, so each line ran as a shell command. They now call agent.launchReplay into the existing workspace with the prompt; the host picks chat or terminal from the user's default and delivers the prompt. Hosts without the launch capabilities get the buttons disabled with update copy. The agent comes from the desktop's own resolution (moved to src/shared). The replay loop and capability read are shared with the workspace-create launch. * test(mobile): repin bridged-parity tallies for the AI-button launch goldens The corpus goes from 787 to 790 goldens: five shell-path goldens are removed and eight agent.launch ones added; one lands in identical and two in result-absent-settlement. * test(mobile): re-record goldens for AI-button launches through agent.launch Repinned baseline to 514ab7f and re-recorded all goldens. Against the branch point: 781 header-only (baseline on all; adapterSha256 on the 48 goldens whose adapter module changed; scenarioSha256 on 3), one body moved (pr-triage-launch: createTerminal + terminal.send becomes agent.launchReplay), eight added (the new launch outcomes and their reply matrices) and five deleted (the shell-path scenarios and their matrices). * fix(mobile): show review notes' agent launch progress and failures, once "New Agent Session" left the sheet open with no progress for the whole launch (up to a minute while a terminal agent readies), so a second tap started a second agent, and a launch that never started or could not be confirmed rejected an unobserved promise, showing nothing. The sheet now closes on tap, the review screen says "Starting an agent...", one launch runs at a time, and every outcome lands in the review screen's status line. Marking notes sent now reads the screen state when the launch settles, so a note written during the wait is not dropped by the whole-list save. * test(mobile): re-record goldens for review notes' launch outcome on the review screen Repins the corpus to 6f30185. One golden body moves: review-create-agent-refused now fulfils with "Workspace not found" in the review screen's status line and the sheet closed, where it previously rejected an unobserved promise and left the status line empty. The other 789 goldens move only their baseline header. * test(agent-status): drop the retired PR-triage terminal send from the identity inventory The phone's AI buttons no longer create a terminal and send the prompt into it (`createTerminalAndSendPrompt` is gone); the host's agent launch delivers it. There is no terminal action consumer left in that file to pin. * fix(runtime): publish saved source-control launch recipes to paired clients settings.get is an allowlist and omitted sourceControlAi, so the phone never saw an agent saved globally for "Fix checks", "Resolve conflicts" or commit recovery and always fell back to the default agent. The host now publishes the launch actions' recipes (agent, prompt template, agent args), normalized so legacy saved defaults are already migrated. A new optional reply field: older clients ignore it, and a client talking to an older host sees none and keeps using the default agent. * fix(mobile): ask to update Orca only when the host answered without agent launch An unread or failed status read settles with no capabilities, which the AI buttons read as an old host and showed "Update Orca on your computer". The update copy now needs a status the host actually returned; an unread one keeps the buttons disabled without blaming the desktop's version. * fix(mobile): send an AI button's saved agent arguments with its launch The desktop's direct launches for "Fix checks", "Resolve conflicts" and commit recovery pass the action's saved agent arguments to agent.launch; the phone honoured the saved agent but dropped its arguments. It now sends them the same way: absent when none are saved, so the host keeps the user's configured defaults. A host that predates the field ignores it. * test(mobile): repin the RPC recording corpus after the launch recipe and availability fixes Repins to fe85e03. All 790 goldens move only their baseline header: no scenario saves agent arguments or reads an unreadable status, so no recorded behaviour changes. * fix(mobile): say the host status is unreadable instead of nothing when it is With the update copy now reserved for a host that answered without agent launch, an unread status left the AI buttons disabled with no explanation. They now say "Could not read this host's status. Go back and reopen it.", the words the mobile web shell already uses for the same failure; leaving the host re-reads its status. * fix(mobile): wrap an AI button's prompt in the action's saved template The desktop renders every source-control launch's prompt through the action's saved template (buildSourceControlRecoveryAgentCommandInput); the phone sent its built-in prompt as is. Now that the host publishes the recipes, the phone renders through the same shared function, refuses an empty result as the desktop does, and offers the rendered text when it could not be delivered. Review notes have no recipe and are unchanged. * test(mobile): re-record goldens for the templated AI-button prompt Repins to 7fd1555. One golden body moves: pr-triage-prompt-not-delivered now carries the prompt as sent (rendered through the action's template) on its prompt-not-sent result, which is what Copy prompt offers. The other 789 goldens move only their baseline header. * fix(mobile): re-read a host status that failed while the connection stayed up A status.get that timed out or was cut over settled the host's gates closed and was never asked again until the connection state changed, so the phone's AI buttons stayed disabled behind "Could not read this host's status" on a link that was working. The gate still settles closed at once, so a failed read never holds the host screen, but it now re-asks in the background with the same backoff the runtime capability probe uses, and opens once a status lands. A reply this app cannot decode is not re-asked. * test(mobile): repin the RPC recording corpus after the host status re-read The status gate change moves no recorded behavior: every golden's body is unchanged and only its baseline header moves to the new pin. * fix(mobile): show a launch's host warning as a note, not an error A launch that went ahead can carry a host warning (a structured chat ignores saved agent arguments, including the '' a template-only save writes). The AI buttons rendered it in the red error line beside a success haptic. The notice now carries it separately as secondary text, and review notes keep saying they were sent. Commit recovery also takes the synchronous in-flight lock the PR triage buttons use, so two taps before a re-render start one agent. * chore(mobile): record the host status re-read timer for React Doctor The status re-read arms one retry timer from inside its read and clears it in the effect's cleanup. React Doctor reports that self-rescheduling shape even in its minimal form, which failed both changed-lines gates. Suppressed the same way as the session startup timers. * fix(mobile): say review notes are waiting for the desktop instead of doing nothing With no live connection, New Agent Session threw from a handler whose promise the sheet drops, so the tap did nothing visible while the button stayed enabled (proven capabilities survive a drop). It now closes the sheet and shows "Waiting for desktop..." as the other AI buttons do. * fix(mobile): stop sending an AI button's saved agent arguments Whether saved arguments apply depends on the route and shell the host settles after the request (a chat ignores them and warns; malformed ones fail after admission), and the desktop sends them only when they apply. The phone cannot know that, so it now leaves them out and the agent's default arguments apply, as before this series. The saved agent and prompt template still apply. * fix(mobile): mark review notes sent through the latest save The sent marks after an agent launch went through the save callback captured at tap time, whose rollback restores the screen from that moment, so a failed save could drop notes written during the launch. It now uses the latest render's save, as it already did for the screen state. * refactor(mobile): own the host status re-read outside the effect The re-read loop lived inside the effect body, so React Doctor could not see its cleanup and needed an inline suppression plus a config allowlist entry. The loop is now a plain function that returns its stop handle, and the effect returns that handle, the same shape every caller of the runtime capability probe uses. Both suppressions are removed; behaviour is unchanged. * test(mobile): record the host descriptor from a background status re-read Pins that the status read records the host descriptor when a re-read succeeds after a failed first read, not only on the first answer. * fix(mobile): show a PR AI launch notice only under the button that launched it Fix checks and Resolve conflicts shared one error, warning and undelivered prompt, so a Fix checks launch whose prompt was not sent also offered "Copy prompt" under Resolve conflicts, copying the fix-checks prompt. Notices are now kept per button. The host availability notice stays under each disabled button, since it explains why that button cannot be tapped. * fix(mobile): say the host status is being retried instead of asking to reopen it The host status gate now re-reads a failed status in the background, so "Go back and reopen it" asked the user for a step that is no longer needed. The review sheet hint uses the same words. The mobile web shell keeps its own copy. * refactor(mobile): run the host status gate on the shared status probe The gate had its own copy of the status probe's retry loop (same delays, same cutover and backoff split, same stop on an undecodable status). The probe now takes an optional callback for each failed attempt, which the gate uses to settle closed on the first failure, and the duplicate loop and its now-unused reader are removed. Existing probe callers are unchanged. * test(mobile): repin the RPC recording corpus after merging main Re-records every golden against the merge commit and drops the three goldens whose scenarios this branch removed, which the merge had restored from main. * test(mobile): re-record the RPC goldens on the merge with main Conflicted goldens were seeded from main and re-recorded against the merged tree; every value either side recorded survives except main's terminal.send in the PR triage launch, which this branch removes. Drops three goldens main still had for scenarios this branch deleted. * feat(mobile): confirm an AI button's agent started, naming the workspace Fix checks, Resolve conflicts and commit recovery now show "Agent started in <workspace>" under the button once the host started the agent with its prompt, so a tap is no longer silent. The workspace label comes from the Source Control panel and falls back to the branch. * test(mobile): repin the recording baseline to the success-confirmation commit (header-only) * fix(mobile): name the workspace in the diff review's AI-button confirmation The diff review screen mounted the PR sidebar without a workspace label, so "Agent started in ..." under Fix checks and Resolve conflicts named the branch while the screen header named the workspace. The sidebar now requires the label so no screen can drop it, and the diff review passes the one its header shows. * test(mobile): re-record the RPC goldens on the merge with main Repins baseline to the merge commit, the last commit to touch a fenced path. Against this branch before the merge, only header fields move: baseline on every golden, and adapterSha256 on the 14 review-action goldens whose adapter main now drives through the review sheet state. No recorded body changed. * test(mobile): re-record the RPC goldens on the merge with main Repins baseline to the merge commit, the last commit to touch a fenced path. Against this branch before the merge only the baseline header moves, on every golden; no recorded body changed. * test(mobile): give the send-sheet stacking test the review controller's host status inputs The merge with main brought in stablyai#22951's stacking test, which builds the review controller without the host capability and status inputs this branch made required, so the mobile test typecheck ratchet failed. * test(mobile): re-record the RPC goldens on the merge with main Repins baseline to the merge commit 03995ae, the last commit to touch a fenced path. Against this branch before the merge only headers move: baseline on every golden, and adapterSha256 on the 14 goldens recorded through the terminal adapter main changed in stablyai#23080. No recorded body changed, and the merged corpus differs from main exactly as this branch did before.
* test(e2e): await renderer recovery after worker exit * test(e2e): publish worker recovery through authenticated hooks * test: keep retired background worker dormant before activation
…yai#23678) * test(claude): expect typed cancellation in queued-send settlements * fix(ci): respect disabled terminal links and await browser recovery * test(e2e): give legacy close client a profile authority * test(e2e): account for frame pacing in pointer latency budgets * test: compare pointer timing on isolated visible display
…d shared hooks dir (stablyai#23500) * fix(opencode): stop OpenCode 2 loading a stale plugin from the retired shared hooks dir Before 1.4.209 Orca pointed OPENCODE_CONFIG_DIR at <userData>/opencode-hooks/shared and wrote a server()-only status plugin there. 1.4.209 moved the plugin to OpenCode's global config dir and 1.4.210 added the v2 setup() export, but nothing rewrote the old file. Shells, daemon-persisted panes and OpenCode 2 background services that still carry that OPENCODE_CONFIG_DIR load only that dir under OpenCode 2 (it replaces the global dir), so the v2 loader rejects the stale plugin with "Plugin must export a default definition with an id and an effect or setup function" and pane status dies. - Refresh the plugin in the retired shared dir (only when it already exists and its content differs) so OpenCode processes started later from old shells load the dual v1/v2 export. Runs on OpenCode pane spawns and on any spawn that inherits the retired dir, even with agent status hooks off. - Drop an inherited OPENCODE_CONFIG_DIR / ORCA_OPENCODE_* marker that points at the retired dir when building a new pane env, so new panes use global discovery. Limitation: an OpenCode 2 background service already running from an old pane keeps its cached copy of the stale module even after the file is rewritten (verified with opencode2 v2.0.18). It must be restarted (`opencode service restart`); a restart from a new Orca pane then picks up the global config because the env is stripped. * fix(opencode): harden legacy plugin repair and inherited config cleanup * fix(opencode): preserve daemon-owned user config during legacy cleanup * fix(opencode): sanitize inherited sources and repair unseen legacy copies * test(opencode): update shared PTY mocks for legacy repair * test(opencode): annotate shared repair mock signature --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
… status store (stablyai#22553) * refactor(native-chat): the Codex acquire names its turn-boundary methods as a set Behavior-neutral: the same two methods stamp receipt time. Keeps the file under the size limit once the child-work sink lands. * feat(native-chat): Codex sessions write their subagents into the host status store A Codex child thread and each persistent command become host child records, fed through the same delivery, ingest and reducer the Claude lane uses. The child's own turn decides it: turn start is live, turn completion settles it with the outcome Codex reports, and a follow-up turn reopens the same record as a new run. Its open tool call, last message, usage and waiting-on-user flag come from its own thread's frames. A parent turn ending settles nothing. * fix(native-chat): close a Codex child's tool call by its item id alone A completion frame need not restate the tool it ran, so reading the tool name before closing left the call open and the record naming a finished tool. * test(native-chat): pin the Codex child-work evidence and every hop to the host's records Child turn start/end/follow-up, open tool call, last message, usage, waiting, the persistent command a child owns and its monitoring display, a primary turn end settling nothing, and session end. End to end through the real adapter: evidence after the journal and the legacy republish, and the parent state the records imply equals today's at every frame of a scripted session. Through the production runtime: a Codex session's child work reaches the status sink under its own address, and a provider exit ends it there. * test(native-chat): a Codex child's new run never inherits the last run's open call * test(native-chat): a Codex session with no child-work sink holds no evidence * test(native-chat): deliver a Codex child's announcement twice, as Codex does, before counting edges * refactor(native-chat): hand the Codex producer's pending edge over directly * fix(native-chat): name every Codex turn state in the outcome map; type the runtime test's fake opener * fix(native-chat): a Codex child's turn ends on the error that ends it, or on its thread closing Codex can end a child's turn with no turn/completed: an error it will not retry is that turn's own end (the verdict the transcript already settles the same turn on), and a closed thread ran its last turn. The executions, the one owner of child turn state, now end the turn on both, so the strip drops the child and its record settles (failed, or unknown for a close) together, instead of reading working for the life of the session. A systemError status is not an ending: Codex raises it for errors that leave the turn running. A child fact whose frame names no turn now belongs to the turn the child is running, instead of counting for every run. * test(native-chat): a Codex child's turn ending by fatal error or thread close settles strip and record together * test(native-chat): the Codex parity script reads a waiting child through the shared fold's waiting arm * test(native-chat): a Codex child row's journal attempt is its record's generation The journal numbers a Codex child's runs by the turns it observed on the child's thread; the host record numbers them by the runs its evidence opened. Both are keyed by the child's own turn id, so they must agree run for run, including when Codex reports the child's first turn before the spawn that announces it. * test(native-chat): a Codex session's end settles its live children and keeps the ended ones The host no longer erases a session's children when its provider goes away: a child still running settles with an outcome nobody reported, and a child that had already ended keeps what it said. The producer tests now expect exactly that, from the close path and from an unexpected exit. * fix(native-chat): a Codex subagent's shell is its open tool until the process exits Codex runs every agent shell through unified exec, so every subagent shell arrives with the source the persistent-command tracker keys on. The producer skipped those items, so a working subagent never named its shell, and an approved command (started on the approval path, completed from unified exec) stayed its open tool until the turn ended. The tracker still records the process separately, so a command that outlives the turn reads as monitoring. * fix(native-chat): a Codex shell becomes a subagent's own work only once it outlives its turn Codex runs every agent shell through unified exec and never says when one is left running, so the producer turned every shell, even a millisecond `rg`, into a command record the moment it started. Each settled into the session's pool of 32 settled records, so a busy turn evicted a finished subagent's record (its outcome row would vanish) and listed dozens of finished shells beside it. A command now becomes a record at the first turn boundary of the thread that launched it while its process still runs: until then it is the agent's open call. A shell that exits within its turn never becomes a record. * refactor(native-chat): child records keep every settled child and can be removed outright Settled child records now stay until the host drops the session's row; the 32-record trim is gone. A producer can say work stopped with nothing to report, and its record (and the handles it answered to) goes instead of settling. Evidence stays host-internal: the producer and the store share one process. * fix(native-chat): a Codex command is live work from its start until its process stops The command tracker is now the one owner of a Codex command's lifetime. It admits every command whatever `source` Codex tags it with (the approval path starts one as `agent`), and ends it when its process exits, when its thread closes (Codex stops the processes first, so no exit ever arrives), or when the session ends. The producer mirrors that one-to-one: a live record from the start, removed when the command stops, never settled. This removes the turn-boundary rule: a command that was only recorded at its turn's end left the parent reading done for one publish when the main agent's turn ended with a shell still running. The parity script now checks the parent at every journal write, not only at frame end. * fix(native-chat): a Codex command whose approval its turn abandoned never ran Codex starts an approval's command item before it asks, and when the turn ends with the question unanswered (the user stops at the approval), it drops the question and never completes the item. The command tracker admitted that start as a running process, so the strip kept a phantom command row and the session row read working until the session ended. The prompt registry, which owns which approvals are still unanswered, reports the command approvals a turn ended without; the tracker ends those commands with the frame that ended the turn. An answered approval keeps its command. * test(native-chat): start the Codex child-work runtime test without the removed hold Main no longer has host.hold: creating the session starts its child, and nothing a viewer does keeps it running. The test attaches and asserts the one child that attach started, then drives it as before.
stablyai#22568) * feat(orchestration): inject the Orca session id into structured children and let the CLI act as it Every structured session's child (native Claude, native Codex, and the terminal view) carries ORCA_AGENT_SESSION_ID and reaches the Orca CLI. The CLI sends the id in the orchestration envelope; when present it is the caller, and a caller flag naming anyone else is refused before any request. The id is stripped from inherited PTY env and from the SSH host-CLI passthrough, and crosses into WSL so the host can refuse the cross-host claim. * test(orchestration): pin session id injection for native Claude, native Codex, the terminal view, WSL, PTY inheritance and SSH * test(orchestration): pin one caller precedence rule across every CLI verb that names its caller Adds the per-verb table (flagless acts as the session; a conflicting --from or --terminal is refused before any request; the session's own spellings are accepted), the enumerated guess population with its positive control, the structured worker's own handle, the identity-less refusal for an older child, the unchanged terminal agent, and the envelope. dispatch-show's --from only fills preview text, so it passes through unfenced and a session's flagless preview names the address the real dispatch writes. * refactor(orchestration): keep the identity-less marker reader to the marker; the id is checked first * test(orchestration): pin that a host refusal of the session surfaces verbatim from the CLI * fix(orchestration): keep the identity-less marker beside the id for CLIs that predate it A CLI older than the id, reached through a global install when a shell rc resets PATH, would otherwise guess a sibling's terminal in a chat that no longer carries the marker. It refuses on the marker instead; a current CLI checks the id first, so the marker never makes a session with an id identity-less. * fix(orchestration): refuse a conflicting --from on gate-list and task-list scoped by --run A --run listing needs no caller, so both handlers skipped the resolver and a --from naming another actor was dropped silently under a session. The conflict check now runs on that branch too; terminal callers are unchanged. * fix(orchestration): name this app's CLI by absolute path for a structured session's login shells A provider can run each command in a login shell: Codex runs zsh -lc, and the profile rebuilds PATH, putting a global install (possibly an older Orca) ahead of the directory Orca prepended. ORCA_CLI_COMMAND, which an agent resolves the CLI from first, is now the absolute launcher in that directory (the native launcher on Windows), so no shell's startup files can swap it. The PATH prepend stays for shells that read no profile. Found by the live coordinator run of the next PR. * test(orchestration): pin a structured worker's CLI command as this app's absolute launcher * test(orchestration): run the zsh login-shell arm in the real-shell lane that installs zsh The ordinary Linux unit lane has no /bin/zsh, so the zsh arm failed there with ENOENT. It moves to a live-shell file registered in the shell-contracts lane; the bash arm keeps running in every lane. The lane guard's detector now also sees a zsh spawned through the ProcessSpec program field, which is how this test escaped it. * fix(orchestration): omit a structured child's CLI command when no launcher resolves, and pin its instance A bare `orca` fallback named GNOME's screen reader on packaged Linux, and an inherited value named another app's CLI. The builder now deletes any inherited value, sets the absolute launcher only when one resolved, and pins ORCA_USER_DATA_PATH so a current CLI dials the instance that minted the id. Renames the marker reader to hasStructuredSessionMarker and records why the terminal view carries the id without the marker. * fix(terminal): name this app's CLI launcher by absolute path in every local terminal ORCA_CLI_COMMAND meant three things by lane: an absolute launcher for a structured session, a bare name for WSL, and nothing for any other terminal, so a structured session's terminal view lost it. Local terminals now get the same absolute launcher the structured lane gets; WSL keeps its guest command name, and a terminal whose launcher does not resolve still gets none. * feat(cli): hand a command to the session's own CLI when another Orca CLI was invoked A login shell can reorder PATH behind a global install, and an agent or its helper script can run bare `orca`, so the binary that answered depended on the agent following instructions. Orca's packaged launchers and bare-orca shims now export ORCA_CLI_SELF (outermost wins). At the CLI entry, when it names a different launcher than ORCA_CLI_COMMAND, the command re-runs once through the named launcher with ORCA_CLI_REEXEC=1 and exits with its status; both variables are consumed so no child inherits them. Dev launchers export no self on purpose, WSL and SSH names never qualify, and a launcher that cannot start leaves the command to run here. The Windows launcher no longer rewrites ORCA_CLI_COMMAND; the legacy ask protocol normalizes its resume command itself. * refactor(orchestration): declare which flag names the caller on each spec and refuse at the CLI entry Each handler hand-classified its --from/--terminal as the caller or a target, and the refusal of a conflicting caller flag ran inside the caller resolver plus two standalone calls for --run listings, so a new verb that read its flag raw would pass a sibling's handle to a pre-session host. Specs now declare identityFlagRoles, the CLI entry refuses a conflicting caller flag once from the spec, the resolver only applies the id-wins rule, and a test fails any orchestration verb that accepts --from or --terminal without classifying it. * perf(cli): keep the session caller check off the actor codec's module graph The check runs at the CLI entry for every command, and the actor codec pulls zod through the session record. Compare the session's own spellings as plain strings instead. * refactor(cli): spell a session's address from the one prefix constant, off the codec's module graph The Orca session address prefix moves to a leaf module with no imports, re-exported by the address codec, so the CLI entry check derives `session:<id>` from that constant instead of re-typing it and still stays off the codec's zod graph. Prose and test names say caller or Orca session id, not actor. * refactor(orchestration): drop the session id's terminal-view spawn now that the handoff is gone The terminal handoff was removed, so no terminal is ever a structured session: - delete the terminal-view identity env and its WSL passthrough, and their tests; - strip the session caller keys from every terminal's env unconditionally; - the CLI's own-address spelling moves beside the injected id in src/shared, with a test pinning it to the address the host's party resolver gives that session. * fix(terminal): run the Codex launch preflight through the CLI the terminal names Packaged Linux names the userData shim in ORCA_CLI_COMMAND, while the preflight ran the bundled launcher behind it. The CLI saw a different launcher and handed the preflight off to the shim, booting Electron twice before every codex launch. * revert(terminal): keep terminals on main's ORCA_CLI_COMMAND and Codex preflight Only a structured session needs an absolute ORCA_CLI_COMMAND; local terminals go back to naming none (WSL keeps its guest command), and the Codex launch preflight goes back to the bundled launcher. The CLI handoff is scoped to sessions, so a terminal's preflight can no longer be handed off and start Electron twice. This reverts commit d2cefb6 and commit dd2853a. * fix(cli): hand off to the session's CLI only inside a structured session The handoff ran whenever an Orca launcher's ORCA_CLI_SELF differed from an absolute ORCA_CLI_COMMAND, so any process with both - a terminal, a script - ran another install's CLI instead of the one invoked: a beta's --version lied, and an AppImage command from a terminal that outlived its Orca failed. It now requires the injected session id, the identity it exists to deliver. The launcher variables are still consumed in every process. * fix(cli): name the packaged Windows command after the handoff decision The launcher stopped writing orca/orca-ide over ORCA_CLI_COMMAND so the handoff could see a session's absolute launcher, which also changed what every Windows terminal's CLI read. The CLI entry now applies the launcher's rule itself once the handoff is decided, so terminals and the legacy ask resume command see exactly what they saw before, and the resume-command reader goes back to its original form. * refactor(cli): decide the session handoff from the CLI's own entry, not a launcher export Every packaged launcher, shim and dispatcher exported ORCA_CLI_SELF so the CLI could tell which launcher ran it, and compared that with the session's ORCA_CLI_COMMAND. Two launchers of the same app are different files, so a session that reached its own app through a global orca-ide on Linux still handed off and started Electron twice, and the export rode artifacts every terminal uses. A structured session now also names the JS entry its launcher runs (ORCA_SESSION_CLI_ENTRY), and the CLI compares its own argv entry with it: any launcher of the same app stays, another install hands off. The launcher scripts, Linux shim and dispatcher go back to main; the Windows launcher keeps only leaving ORCA_CLI_COMMAND for the CLI to name after the handoff decision. * refactor(cli): drop the session CLI handoff; the pinned instance and injected id already bind any current CLI Every current Orca CLI dials the instance ORCA_USER_DATA_PATH names and sends the injected session id in the orchestration envelope, so a bare `orca` that reaches another install's current CLI already acts as the session. An older CLI has no handoff code and refuses on the marker. The handoff only lined up versions between two current CLIs, and comparing two separately derived paths kept misfiring (an AppImage's mount against its registered extraction started the CLI twice on every call). Removes the re-exec, ORCA_SESSION_CLI_ENTRY and ORCA_CLI_REEXEC, and the CLI-side Windows command naming; the packaged Windows launcher rewrites ORCA_CLI_COMMAND again, as on main, inside its own process only. resolveHostCliEntryPath goes back to the SSH passthrough. * test(orchestration): say why the registered worker case pins the handle, now that every session's env is populated
… spawn tag (stablyai#23460) * fix(native-chat): stop signalling processes that only inherited a spawn token A spawn token is an environment variable, so every descendant of a provider child carries it. The Linux-only startup scan treated any carrier no lease claimed as a lost provider child and sent it SIGTERM, which also hit editors, tmux servers and nested Orca processes the agent had started. Remove that scan's killing consumer; the token scan stays for the reservation probe, and recorded owners are still stopped by identity during recovery. * fix(codex): remove the token-scan kill path from app-server teardown Every descendant inherits the spawn token, so killing each pid that carries it can reach processes the agent started that are not the provider. Production never injected this path; teardown always uses the process-group and descendant-snapshot proof. Drop it, its deps, and the now-unused spawn-token argument.
…, never the parent's (stablyai#23605) * fix(native-chat): a subagent's words are presented as that subagent's, never the parent's The journal already names the agent that produced every row, but the transcript projection dropped it, so a subagent's prose rendered as the parent's reply, its tool calls folded into the parent's runs, and a settled turn could fold down to a subagent's words as its only visible answer. The transcript message now keeps the row's producer. The fold keeps each agent's calls in that agent's own run, a turn's answer is the session's own agent's last prose, and a subagent's row names the subagent on desktop, mobile and a worker's transcript text. * test(native-chat): give the window fixture's slot the attribution field it now carries * fix(mobile): read the subagent label the row is given, and pin the caption * fix(native-chat): keep interleaved agents in order and each agent's own run live Review follow-ups: - the fold is main's adjacency fold plus one condition: a row never folds into another agent's run, so an agent's later call stays below its subagent's work instead of jumping back into its earlier row - each agent has its own live frontier, so a parent still inside its spawn call reads as running while its subagent works below it - mobile names no one on a row whose only content is hidden behind its settled turn - a pending question from a subagent keeps its producer - worker reads serve only the producing agent's id, bounded like the roster key that names it, and drop the provenance fields - the single-message worker formatter is private, so no caller can drop names
…ly while visible (stablyai#23592) * fix(tab-group): measure fallback pane geometry once per tab group, only while visible * refactor(tab-group): derive the shared resize listener's lifetime from the source map The map already drops empty groups, so a separate counter was a second copy that could disagree.
…zing both (stablyai#23585) Status-row change detection stringified two full IPC payloads on every status write, including an up-to-8 KB lastAssistantMessage re-posted on every OpenCode streamed part. Compare the same published field set with the existing structural-equality helper, with a same-reference fast path. Linear: STA-7432
…ablyai#22644) * fix(ssh): don't overwrite remote agent config after a failed read A flaky read was treated as an empty file, wiping the user's config. Fixes stablyai#22638 * test(runtime): model remote missing-config reads as relay ENOENT errors The runtime harness stubbed isENOENT as code-only, and the remote Codex startup specs rejected with a generic error that only passed while any read failure seeded an empty config. Use the real isENOENT and the message-only shape the relay actually delivers. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…stopped (stablyai#23466) * fix(codex): the provider supervisor outlives its provider group when stopped A signalled supervisor forwards the signal to the provider group, escalates to SIGKILL after the grace, and exits only once the group is gone, so recovery's proof that the recorded pid is dead also proves the provider is. It refuses to spawn when its parent is already not the owner named in its spec, and watches that owner rather than whichever parent it first saw. The grace is a spec field. Recovery's SIGTERM stage now outlasts the supervisor's own stop, since a SIGKILL that lands first cannot be handled and leaves the group running. * fix(codex): a closed owner pipe no longer ends the supervisor before its provider group When Orca dies, the supervisor's stdout pipe has no reader. Provider output in the window before the parent-death watch fired raised an unhandled EPIPE that exited the supervisor with the provider group still running. * fix(codex): bound the supervisor grace so recovery's SIGTERM stage always covers it Recovery sized its SIGTERM stage from the default grace, so a launch with a longer grace would be SIGKILLed mid-stop and orphan its group with no test noticing. The spec now refuses any grace above one exported maximum, and recovery derives its SIGTERM stage from that maximum. * fix(codex): every supervisor stop asks the provider with SIGTERM first Owner death, stdin end after the grace, and a signal to the supervisor now all take one path: SIGTERM the provider group, SIGKILL it after the grace, and exit only once it is gone. The signal handlers are registered before the provider is spawned, so a stop that lands in the spawn window still reaps it. The longest stop grows to two graces plus the reap wait, and both recovery's SIGTERM stage and the connection's graceful close now wait that long before forcing, since forcing the supervisor sooner can orphan its group. * fix(codex): give the provider 1 s after stdin end and 3 s after SIGTERM to flush before SIGKILL The supervisor's stop was stdin end, 1.25 s, SIGTERM, 1.25 s, SIGKILL. Codex now gets 3 s after SIGTERM to flush its state. The two graces are separate constants, the longest stop they derive becomes 5.5 s, and a test keeps it inside quit's 8 s child-eviction bound. * test(codex): count eviction's pre-stop drain in the quit budget test Eviction drains the sink for up to 1 s before it stops the child, inside the same 8 s bound.
…ai#23492) * fix(runtime): retire an exited terminal before its stream end An exit's durable retirement became asynchronous, so onPtyExit released the terminal stream before the retirement landed. A paired client answers a stream end by re-activating its pane; that activation still found the exited leaf, materialized it under the same session id, and registerPty dropped the pending retirement. The exited split pane came back as a fresh shell. The exit now stages the retirement into the in-memory session and publishes it synchronously, then notifies exit listeners, and only then makes it durable. A failed durable write is logged and left in memory for the next profile write instead of being rolled back, since the process is gone either way. This removes the pending-retirement latch and its post-await incarnation fence: there is no longer a window for them to guard. * test(runtime): a failed exit retirement still reaches disk Pins the no-rollback contract through a real Store and SQLite authority: when the retirement's own durable write fails, the in-memory retirement is carried by the next unrelated profile write, and by the app-quit flush when no other write happens. The delayed authority fixture can now fail its next write, and the acknowledged-retirement fixture reads the database a relaunch would load and models the quit flush. * test(runtime): a stream end observes the exit retirement already published The re-activation check alone passes with the listener ordering reverted, because activation awaits before its lookup. Record the session binding and publication count at the moment the exit listener fires so the ordering itself is pinned. * fix(runtime): an exit cleanup fault still ends the terminal stream * perf(runtime): exits retired together share one durable write * test(runtime): a refused staging write still retires the pane and ends the stream * refactor(runtime): describe exit retirement as staged, not durably accepted The retirement result is staged in memory before any write, and the removable-surface comment and the replacement-admission test name still described the old publish-after-durable rule.
…ai#23701) * fix(terminal): retain typing while a parked remote pane reattaches * test: persist restored remote terminal screenshots * fix(remote): buffer recovery reconnect input * fix(remote): retain input across restored pane attach * fix(remote): flush attach input after subscription * fix(remote): flush reattach input after attach readiness * fix(remote): stop buffering after reattach readiness * test(remote): trace parked reattach input lifecycle * test(remote): forward paired client lifecycle diagnostics * fix(remote): preserve restored typing before connect starts * chore(i18n): refresh runtime required catalog * fix(i18n): ship compact agent runtime label * fix(i18n): merge required label into existing sidebar catalog
…th no other rest signal (stablyai#23598) * fix(runtime): reopen the quiet-foreground tui-idle lane for agents with no other rest signal A tui-idle wait could never settle on a pane running amp, goose, crush, kimi, qwen-code, rovo, auggie and other agents whose titles Orca cannot classify: the quiet-foreground lane was closed for every launched agent, and it was the only lane those agents could reach, so worker start failed at agent_readiness after 60s. Model each agent's rest signal, derived from the tables that already encode it (synthetic ready titles, the title classifier, the DSH hook and Muse ready screen lanes). The lane stays closed where a stronger signal will arrive and reopens for agents with none. On a reopened lane, silence counts only after the TUI has painted: an agent that has painted nothing is still booting. Linear: STA-7440 * fix(runtime): count only the command's own output as an agent's paint on the tui-idle foreground lane The after-paint lane accepted any output, and the shell's prompt and echoed launch command always land before the agent starts, so a silently booting agent could still settle and lose its first prompt. The runtime now reads the shell integration's command-start marker and requires visible output after it; panes whose shell emits no marker keep the any-output rule. Also skip the foreground-process read while the pane cannot settle, and register the new title-classifier call site in the pane agent identity inventory. * fix(runtime): classify Freebuff's rest signal and skip the backward marker scan on chunks without one Main added the Freebuff agent after this branch point, so the full per-agent rest-signal table no longer matched on the merge ref. Freebuff derives `none`: its screen reports a first-party `done`, which tui-idle trusts only for DSH, so the quiet-foreground lane is its only one. The command-start scan ran a backward search over every PTY chunk; a forward check first cuts that to the cost of a plain substring test on chunks with no marker. * fix(runtime): classify Qoder's rest signal after merging main Main added Qoder with its own readiness branch returning a boolean quiet-foreground flag; map it to the lane type and classify Qoder by its ready screen so the full rest-signal table and lane-agreement check stay exhaustive. Say what `none` actually means: no stronger lane tui-idle trusts, not no hooks at all. * refactor(runtime): track command paint with the shared OSC 133 scanner The command-paint tracker had its own split-unsafe 133;C parser. Reuse the chunk-boundary-safe scanner, which now reports where in the chunk the marker ended, so a marker split across reads is still found. Correct the unmarked-launch list: bash and zsh mark typed launches after the echo. * fix(runtime): drop command-paint state on an output gap or a new process A dropped chunk can cut a command-start marker in half, and the scanner's carry then completes it on unrelated output after the gap, leaving the pane waiting for a paint that already happened. Reset it with the other cross-chunk carries. * fix(terminal): keep the command-start offset out of renderer lifecycle callbacks
… starts its agent (stablyai#23935) * fix(native-chat): every journal append reaches the chats that are open A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted. * test(native-chat): an epoch replacement reaches the open chat * test(native-chat): each row reaches an open chat once, and a live handle enters only through the map * test(native-chat): give the legacy-lease store test a tab id so the backfill cannot supply its rewrite The seeded record had no surface tab id, so the next open backfilled one and that rewrite alone made the no-op transaction write. The test passed with the legacy-lease rewrite signal removed. * test(worktree-activation): restore the OMP surfaced-agent resume test The handoff removal deleted it alongside the terminal-owner tests, but it covers the surfaced-PTY block that still guards resume, including an agent whose ownership is unknown. * perf(native-chat): a publish behind a delivered commit reads nothing Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read. * test(native-chat): state why the teardown test's fake journal is safe to cast * docs(native-chat): say mutation admission checks only the writer lease * docs(native-chat): drop the send rebase from comments that still described it * fix(native-chat): a message is accepted, then delivered A send to a chat with no running agent restarted the agent inside the send call, before the message was recorded, so the client waited for the whole start and a failed restart refused the message. Claude held prompts sent during startup, and those could settle as "unconfirmed". A send is now accepted inside the session's serialized queue: one ledger row and one submission row marked handoverRecorded, published, answered pending. A per-session delivery loop exists while a message is queued. It starts the agent through the same serialized attach a hold uses, waits outside the queue for a Claude child to prove its start, and hands the oldest queued message over as its own serialized step, writing dispatch{pending} before the adapter call. A start it needed and did not get writes one error-tone row and rejects every queued message with the same words; a start Stop cancelled writes none. Settlement follows from the rows. A queued message is provably unwritten, so a close, an eviction or an exit rejects it. A handed-over message stays in doubt. A queued row at or below the sequence a handle found when it opened was left by an earlier process and is rejected at open, with no latch. Stop withdraws queued messages with no writer lease and no fence. An attach failure keeps the conversation open, and the attach adopts its journal. Owed work counts the loop and queued rows. A compaction or rewind found prepared when a conversation opens was started under a child this process no longer has, so the open settles it rather than leaving it to refuse every send until a view attaches. The open cursor is scoped to its epoch, because sequences restart when an epoch is replaced. Deleted: restart-before-admission, recordFailedRestart, the fence rebase, Claude's startup gate, the attach's forget on failure and its own crash boundary. Clients without agent-session.accepted-send.v1 get their reply held until the handover; the desktop and paired desktop lists advertise it. * fix(native-chat): settle queued messages only for the child that ended A child that proved its start and then exited before its message was handed over left the message queued: the exit settlement returned early when nothing else was in flight. Delivery then started another child for it, and a child that died the same way started another, without end and without a row. A retried settlement for an earlier generation, run by the attach that delivery started, did the opposite: with that generation's turn unfinished it rejected the message queued for the child being attached. The settlement now takes the rejection for queued messages from its caller. The unexpected exit and the eviction pass one, and it applies even with no other work in flight; the retry for an earlier generation passes none. * fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. * perf(native-chat): the recovering open reads the journal once Every conversation open now goes through the recovering open, including the read restore of every chat at startup, which used to replay its journal once. The recovering open replayed it twice: once to probe it and again inside the open. The probe is now handed to the open as its load. * fix(native-chat): an attach that fails after indexing its child leaves no child behind A failed attach now keeps the conversation open, but a failure after `onAttached` indexed the child (the rewind or compaction recovery, or the attach's own success record) left that entry claiming a child the failure path had already released. The next send found the phantom, skipped the start, and wrote at a fence the journal had moved past, so the message stayed queued for good. The entry now drops the released child and its event sink, and follows the record's fence, as a failure before indexing already did. * fix(native-chat): a withdrawn message shows no error, and a rejection outlasts the send's answer The error strip for a message the host accepted and then did not deliver matched the entry before the outbox reconciled, so a Stop's withdrawal, which the reconcile drops, showed "Orca could not send your message" with nothing to retry. It now reads the reconciled entry. A rejection the journal records before the send's own pending answer lands is final as well: that answer no longer puts the entry back to dispatching with no Retry. * fix(orchestration): a structured worker whose agent outlasts the preamble wait is left unknown, not torn down The preamble waits for its submission to be delivered while the worker's agent starts. When that wait ran out it threw operation_unknown, and the failed-start teardown then closed the session, which rejected the very preamble the host was about to deliver. It now reports a turn start nobody observed yet: the worker is start-unknown with its session kept, the host delivers the preamble when the agent starts, and the worker's report settles the dispatch as for any unobserved start. The receipt no longer suggests reading a screen a structured worker lacks. * fix(native-chat): a message rejected while its chat was closed reads as not sent A remount reads an entry it left dispatching as unconfirmed. When the journal had rejected it meanwhile, as a failed start or a quit now does, the reconcile left it unconfirmed: it blocked every later message behind a Retry and no reason, and the delivery probe, seeing the journal already answered, never ran. The reconcile now settles it as rejected like a dispatching one. * test(orchestration): name why the readiness settlement fakes are cast * fix(native-chat): keep each pane's own fence on frames so a failed restart is not resent * docs(native-chat): drop the fence from the admission the send effects run behind * docs(native-chat): give the fence move on release the reason that still holds * docs(native-chat): stop citing a write fence check in launch and mailbox comments Three places still gave the removed fence check as a reason: the launch replay said admission puts the ledger ahead of the fence, the launch surface said a send must name the lease it was admitted against, and the direct-mailbox path said the lease fence decides whether delivery is safe. Admission now checks only the writer lease. * refactor(native-chat): the provider child is its own record A conversation now outlives any number of provider children, so the child is one record on the conversation's entry instead of five loose fields beside its journal. It is written in one place: indexed only once an attach has fully succeeded, and ended through one function that an exit, a failed re-attach, a Stop and an eviction all share, matched on the child's generation and fence. - A failed attach writes no child, so there is nothing to unwind: the field unwind and the fence patch after it are gone. - Conversation writes read the record's fence, the way mutation admission already does; a child's own writes use its fence. The four stored-fence patches, and the settlement retry's overwrite of the conversation's fence, are gone. - The owed wind-down is its own tombstone, carrying the child it is owed for, and is no longer dropped when an attach replaced the whole entry. - Stop on a child still proving its start stops only the child: its lease goes back and the chat is told it is idle, but the journal, the holders and the readers stay. Close is that stop plus the conversation's close. - The settlement retry uses the conversation's own journal, opened through the host's one open. * fix(native-chat): the delivery loop alone settles a message its start or child failed A queued message was settled by whichever path happened to end the child first: the loop, the unexpected exit, eviction's work settlement, the open's leftover rule, and the startup branch that rejected every pending row. That gave two failure rows with different tones for one start, a loop that could hand over to a different child than the one it waited on, and a Claude start that died while starting reading unlike every other failed start. - The loop remembers the child it waited on. At handover, if that child is gone or replaced, it reads how it ended: a Stop continues; anything else writes one failure row and rejects every queued message with the same words, then stops. A child still starting whose start the adapter says did not land fails the same way. The exit, eviction and the settlement retry only settle the handed-over and legacy rows of the child that ended. - One failure row, always an error, keyed by the start. A start a view began that dies with nothing queued writes the same row through the same builder, so a second report revises it. - The open no longer rejects leftovers; the loop's first step does, and the open wakes it. - `awaitStarted` answers why a start did not land, so the row says it even when the loop sees the failure before the exit is processed. - Quit closes every conversation the way closing a chat does: what is still queued is rejected as closed, with or without a child, and a start the loop already has in flight is waited for so the child it produces is stopped rather than left behind. * refactor(native-chat): a stopped child ends on the one reading of its stop The eviction step reads a stop's result through `stopAgentSessionProviderRoot` and hands that verdict to the child's ending, so the host never forms a second view of whether the root is gone. Every ending carries it: a stop's comes from that reading, an exit's root is gone by definition, and a failed re-attach passes what its release saw. The end-of-child record can therefore also carry a stop whose root was not seen to go, which nothing ends on yet. * feat(native-chat): the host says it accepts a send before any agent has it The host now lists agent-session.accepted-send.v1 among its own runtime capabilities, the same string capable clients already send. A client can then tell a host that answers a send at acceptance, and admits a Stop with no writer before a turn starts, from an older one that still restarts the agent inside the send. Additive: an older client ignores a capability it does not know. * refactor(native-chat): an attach never opens a journal of its own The attach adopts the conversation's open journal, which outlives it, so it no longer opens one for a direct caller either. That leaves nothing for a failed adopted import to close, and the flag that told the two cases apart is gone. Tests that attach without a host open the conversation the way a host does. * fix(native-chat): a moved fence resends nothing on a host that accepts first The outbox treated any fence change as a new owner: it dropped the answer of a send in flight, queued that send to go out again under the same id, and unblocked a refused head. On an older host that is how a send the restart refused, unrecorded, gets another try. On a host that records every send before it starts an agent, a fence moves because that start ran, so the same rule resent into every failed start. With a fence stamped on every frame, that became a loop. The outbox now reacts to a fence change only when the host has not advertised that it accepts a send before any agent has it. On such a host, only a Retry or a new send goes out, and a failed start reaches the client as a rejected message it keeps with its Retry. Against an older host, or before one has answered, the outbox behaves as it did. Desktop and paired web share this hook. * refactor(native-chat): a child's end says whether the user or the host stopped it The end-of-child record's cause now tells a user's Stop from the host stopping the child for a cause of its own: `user-stop` and `host-stop` replace `stop`. The delivery loop goes on after a user's Stop, as before, and fails the start it was waiting on after a host stop, with the one error row and every queued message rejected, in the stop's reason when it gave one. The reason stays description only. Stop passes `user-stop`; nothing passes `host-stop` yet. * fix(native-chat): a chat whose only work is a queued message is not offered for resume A message accepted while the agent was starting counts as working in the chat, and quit rejects it as never sent. The teardown snapshot read the same working rule, so a relaunch offered to resume a chat whose agent never had the message. The snapshot now reads only what was handed over. * fix(native-chat): the conversation outlives its agent Opening a chat no longer starts its agent. A conversation is reached through one host accessor that opens its journal at rest, and a send is what starts the agent, through the delivery loop. One idle sweep, every five minutes, stops an agent that has been quiet for thirty minutes and owes no work, then drops an open journal handle that is only a cache. Its record, tab, status row and readers stay. - hold and release are no-ops; hold still builds the host for shipped mobile builds. - The holders, the holds, the release clock and the exit respawn are deleted. - Options, the model list, the goal and the context meter answer at rest; a model pick at rest is recorded as intent for the next start. - Compact, rewind, clear and goal changes start the agent first. A send does too when a rewind is still in doubt after the conversation opens. - Orchestration routes mail and group addresses on ownership (the record plus the chat tab), not on whether the process runs. An open dispatch keeps its worker running. - The restart continuation is a send; Resume all holds each slot until the message is handed over or rejected. - A read error never replaces a loaded transcript, and shows the host's own words. * test(native-chat): type the queued-message fixtures in the resume-offer tests * fix(native-chat): a start that dies while a message waits on it is that message's failed start Opening a chat's tab starts an agent for the view, and a send accepted meanwhile waits on it. When that start died, its exit wrote the start's error row and left the message queued, so the delivery loop started a second agent into the same failure and wrote a second row. A child's end now records where the conversation's journal stood, and the loop settles a message accepted before a failed start ended with that start: one row, under its key, and no second start. A message sent after the failure still gets a fresh start. * fix(native-chat): a request that failed reads as failed A structured chat whose only message the agent's start refused read as a green finish, and a cancelled structured turn did too: the host published a verdict only for turn records, and structured rows carried no `interrupted`. The host projection now reads the session's latest request: its turn's outcome, or `failure` for a send the agent or its start refused. A send that was withdrawn, or left undelivered by a restart or a close, fails nobody and makes nothing listable. The ingest publishes `interrupted` as the hook lanes do, and every reader decodes the verdict through one accessor, so a failure reads Failed on the dot, the rollups, history and `worktree ps`, behaves like a cancellation in every clean-finish policy, and notifies as "failed". * docs(native-chat): say what an attach's open conversation and unconfirmed ids are now * test(native-chat): a verdict change republishes the mobile status projection * refactor(native-chat): the store's retention trigger keeps its flag compare A verdict change always moves the completion clock the same check already reads, so a second verdict compare there caught nothing new. * test(native-chat): a user message the provider journaled keeps its session listed * test(native-chat): pin what a failed start settles, and what a resume offer names A view's child that dies while a sent message waits settles that message only when it died starting and no child has taken its place: a proven child's crash, or a second start since, gets the message delivered. The resume offer names the handed-over message, never a newer one still queued. * test(native-chat): the failed-start pins fail on what the message became, not on a timeout * fix(native-chat): a restart offer ends when the chat's agent starts again The offer used to end only when the chat's newest user message changed, because opening a chat started its agent and that start could not be told apart from real activity. Opening a chat starts nothing now, so the host reads the fact it already publishes: a chat's status row goes from not host-owned to host-owned exactly when its agent is started. At that edge the offer and any failure record for the chat are withdrawn, unless the start is a resume action's own (its continuation is the oldest undelivered message). A continuation and a message racing to be first are decided at acceptance: the continuation is refused, quietly and with nothing filed, when any other message was accepted since the restart. A failed continuation start leaves the offer retryable, and each resume action sends its own message id. Deleted: the newest-user-message comparison, its journal reader, the continuation filter, and the failure ledger's own "answered by the chat" check. The marker still carries its message id for one release, so the previous build can read it. * fix(runtime): end a transcript stream when its client unsubscribes Desktop: the IPC subscription controller was dropped as soon as the streaming handler returned, which for most streams is right after it binds. A later runtime:unsubscribe then found nothing to abort, so the host kept the subscriber and derived and sent every publish to a channel no one listened to. The controller now lives until the renderer unsubscribes, resubscribes the same id, or goes away. Mobile: disposing an agentSession.subscribe stream now sends agentSession.unsubscribe with the stream's frame id, so the host ends that subscriber and leaves a sibling stream on the same socket running. The direct path now passes the frame id the relay path already passed. * fix(native-chat): a late provider-session update keeps a failed recovery record failed A provider-session heartbeat that rewrites a completed recovery record kept its interrupted flag but dropped the outcome it was copied with, so a live failed checkpoint read as a clean finish until the next status write. * test(orchestration): the preamble's host stub is typed, not cast The preamble send now takes only what it reads of the host, the send, the settlement wait and the record's fence, so its test builds that host with real types instead of `as never`. * test(native-chat): the terminal-bell check asserts the renamed verdict field The bell notification test still checked for agentInterrupted, which no longer exists, so it could not catch a verdict leaking into a bell dispatch. * fix(native-chat): a failed turn ranks like a completion for attention Attention readers (completion time, Smart Sort, sticky retention, Cmd+J Recent) now demote only a turn the user stopped. A failure is news the user has not seen, so it keeps its completion time, ranks in the Done class, stays retained after its pane goes away, and a retained failure reads failed in the worktree rollup instead of done. Clean-finish policy (hibernation, pane ownership, the value moment) still treats a failure like a stop. The retention trigger compares verdicts again: success -> failure no longer moves the completion clock. * fix(native-chat): one fact ends a restart offer: the chat moved on since the restart The offer is live while no other message has been accepted in the chat since the restart and its agent has not proved a start since. The offer list, the resume's reservation check and the continuation's acceptance check all read that one fact, so a message whose start then failed withdraws the offer too, and a stale click finds nothing to act on. The fact is read off the conversation's open handle, which the restart closed, so it is retired durably whenever it may have changed: a message accepted, a start proven. A close and reopen within the same run therefore cannot bring the offer back. A continuation rejected before it reached the agent does not count, so a retry after a failed start still runs. Deleted: the quit-time gate on withdrawal, which changed nothing because the withdrawal and the quit's own offer write share one queue; the per-action "withdrawn" flag and the separate acceptance check it paired with. * test(native-chat): an older build reads the restart offer this build records The offer lives in a file the previous release reads after a downgrade. Pin that against the pinned release's own capsule, and run the lane when the marker or the capsule changes. * fix(native-chat): read a restart offer against where the journal stood when it was taken "Since the restart" was read off the conversation's open handle, which the idle sweep closes: after a reopen, a message the user had already sent looked older than the handle and the withdrawn offer came back. The offer now records the journal position (epoch and sequence) at the moment it is taken, and a message accepted after that position, or a journal on another epoch, means the chat moved on. That is derived from the journal, so it holds across any number of closes and reopens. An older build's offer has no position; only a start withdraws it. Because the message half is now durable, the offer is no longer rewritten in the recovery file on every accepted message; a proven start still writes it, since only the host that saw the start knows of it. * test(native-chat): wait for the listing's retire write before reading the recovery file * refactor(native-chat): every journal row states which turn it belongs to Rows gain a turn scope stated by the write that creates them: the open root turn, or the conversation. A queued message takes its scope from its handover. Rows stored before scopes existed are placed on replay by the root turn open when they were created, so no persisted state is needed for them. Rewind keeps each retained row's scope and producer, so a subagent's row stays its own. * fix(native-chat): keep the terminal-backed chat's read error over its local echoes Messages winning over a read error is right for the structured chat, whose read retries and whose messages came from the transcript. The terminal-backed view assembles its list from local echoes too (a launch prompt, a pending send), so a failed read there showed only those bubbles and no error. Only the structured pane now keeps messages over an error. * fix(native-chat): a start retries the exit settlement a failed journal write left owed An agent exit whose journal settlement write failed releases the lease latched until a retry lands. Reopening the chat used to be that retry; with reveal now only opening the journal, nothing retried it before the next app launch, and every send was refused. The start the send needs now runs the retry first, where the attach would. * fix(native-chat): a failed main agent reads failed while its subagents still work The verdict is now read from the main agent's own state, not the folded row: a main agent that is done and failed has a verdict even while its subagents keep the row working. Without mainAgent (history, worktree ps, older hosts) the old combined-done rule stands. Display marks the verdict through agentVerdictDisplayMark: a failure outranks every combined state on the agent's dot, label, tab badge, dashboard and activity rows; a stop marks only a done row, so a successful or stopped main agent with live subagents still reads working. Subagent rows keep their own state. The worktree card, terminal tab and Cmd+J rollups share one pane fold and rank a pending question, then failed, then working, monitoring, interrupted and done. worktree ps publishes the main agent's outcome on a working row, and the mobile mirror reads it. The store's change check, the paired-client mirror's equality and its epoch now see a verdict change on a working row, which otherwise moves no state or clock and left the worktree card reading working. Clean-finish policy is unchanged: a working row is never hibernated and has no completion time. * perf(native-chat): answer the owner check without opening the chat Worktree activation calls agentSession.handoffStatus for every chat tab in the worktree, and the answer comes from the session record alone. Reaching it through the accessor opened each resting chat's journal (a full read, the crash-boundary write and a restored status publish), then kept it open for the idle window. It now checks the record and the adapter's support, as before this series, and opens nothing. * fix(native-chat): a read waiting on the session lock opens nothing once quit began The accessor checked for quit before queueing the open, so a read queued behind a session task ran its open after teardown had begun and indexed a journal no teardown step would close. The check now runs at the open itself. * test(native-chat): pin stated turn scopes, the upcast of unscoped rows, and rewind attribution * fix(native-chat): /compact is a message the chat sends, run as a turn of its own The conversation command RPC now accepts /compact into the queue like any send and answers once it is handed over. The delivery loop opens the command's own turn, starts the provider on it, and waits for the provider's end off the session's queue, so messages typed meanwhile are held and delivered after it, even when it fails. It settles by re-reading the journal: a child that died meanwhile already wrote the verdict. Stop ends the command at once. The 180 s completion window, the unconfirmed row and the recovery of an older build's compaction record are gone; that record no longer gates anything. On Codex the provider turn the command opens is claimed into the command's turn. * fix(native-chat): read a failed resume's chat before calling it retryable Whether a failed resume is retryable is the offer's own rule: the chat has not moved on since the restart, read from its journal. The failure list read it only for a chat already open, so once the idle sweep closed a chat the user had moved on in, its failure showed Retry again, and the click did nothing. The list now opens the failed chats first, as the offer list does. * test(native-chat): type the provider event sink the settlement test reaches for * docs(native-chat): the worktree ps outcome comment no longer claims old hosts send it The field is new: an old host sends no outcome at all, so a reader falls back to interrupted. The removed clause said old hosts send it on done rows, which never shipped. * fix(native-chat): say the structured read keeps trying only where it does The structured pane's "Orca keeps trying to load it" line never showed: the view state filled in an untranslated fallback whenever the read error had no text, and the empty state prefers any message. The view state now leaves the message out, so the structured pane shows that line and the terminal-backed pane its own translated one. Mobile's structured lane does not resubscribe after an error frame, so it no longer makes the claim. * fix(native-chat): rows group under the turn their record names, not the one above them Each row's turn is the turn its stated scope names, anchored on the entry that opened it, or on the turn itself when the provider opened it unasked. So /compact groups its own rows and the previous turn is untouched, a message typed into a running turn joins it, and a provider-resumed turn folds under its own Worked-for. A row reporting how a turn ended, an error or the compaction separator, never folds. Desktop and mobile read the same keys; a host that states no scope keeps today's positional grouping. * test(native-chat): await the send's settlement instead of polling for the start The at-rest send tests polled for the provider start with vi.waitFor's one-second default, which a loaded machine outran. They now await the host's own settlement of the message. * docs(native-chat): the status-store listing rule names provider-journaled user messages * fix(native-chat): a restart offer resumes any time after the quit, and knows its own continuations The continuation's message id was dated by the quit, and the ledger refuses a new id dated more than a day back, so Resume or Retry a day after quitting was always refused (on main too). It is now dated by the resume action. Telling a rejected continuation from the user's own message read the operation ledger, whose rows expire after about a day; after that a failed resume stopped being retryable. The offer now records the continuation each action sends on its own capsule entry, bounded to the newest 16, so the ids end with the offer. The ledger read is deleted. * fix(native-chat): a /compact is not a request the sidebar, notifications or restart resume report The sidebar's prompt, preview, verdict and instant, the turn-completion feed, and the restart-resume marker read past a conversation command and its turn to the last real request, so a /compact neither notifies nor re-dates the row, and a command in flight is never offered as work to resume. An older client shown a command's turn in the legacy form names the session's own agent. * fix(orchestration): route no mail to a structured worker its orchestration released A structured worker is routed on ownership, and a resting worker's lease is released, so ownership held while its chat tab stayed listed. A worker the coordinator abandoned and then released, found at rest by the release, therefore still took peer mail and @worktree: broadcasts, and each one restarted its agent. Routing now also reads the orchestration's own resource row: once it is released, direct mail, group addressing and worker-show's addressable answer drop the worker, as they would a terminal worker whose terminal closed. The chat tab stays, and nothing new is stored. * fix(native-chat): a failed retry names the user's prompt, not Orca's continuation A resume's continuation is written to the chat before its start, so after a failed attempt the chat's newest user message is that rejected continuation. A second failure then showed Orca's own restart text as the chat's prompt. A retry now keeps the prompt its first failure named. * test(native-chat): pin what a conversation command's admission refuses at rest and at handover * test(native-chat): tests merged from the base state which turn their rows belong to * fix(native-chat): a refused send notifies failed through the completion feed The host's completion feed followed only the newest turn, so a send the agent or its start refused, which creates no turn, read Failed on its row but sent no notification. The feed now follows the session's latest request, read from the projection the status feed already makes for the commit: a turn keeps its id, a refused send is named by its journal item key. It announces only while the session is idle, as the row reports a verdict, so queued sends refused one commit at a time notify once, and a withdrawn send falls back to a request already announced. * fix(orchestration): read the released row optionally, as the authority does worker-show's observation called the row lookup directly, which a runtime double without it threw on and failed the structured tab-retirement release. * chore(native-chat): one import per module and no unexplained casts in the turn-scope changes * test(claude): pin which turn a Claude row joins, including a subagent's after the turn ends * fix(native-chat): the status bar drops a restart offer the chat moved on from The renderer re-read the host's restart offer only when a failed chat showed activity, so after a message withdrew a pending offer the host answered no chats while the status bar kept counting one, and clicking it opened nothing. The same watch now covers pending offers: a status change in an offered chat asks the host again, once. * fix(native-chat): a refused steer is read from the turn its handover named The latest-request reader decided whether a refused send had joined a running turn by comparing host clocks: its handover time against the previous turn's end. The handover row now states the turn it delivered into, so the reader reads that instead and the clock comparison goes. A journal written before handover rows stated a turn is scoped on replay from the turn open when each row was written, which can differ from the clock reading only when a send and a turn's end share a millisecond. * fix(mobile): the native-chat controller contract carries the turn journal The controller and overlay already pass nativeChatTurnJournal, but the contract type never declared it, so mobile failed to typecheck. * fix(native-chat): the live turn is the running turn, not the newest user row A turn the provider opened on its own (a background wake, a resumed turn) anchors on its own record, but the list still treated the newest user row as the live turn. While such a turn ran, the settled user turn before it lost its duration and the running turn's own rows were drawn as settled, so its tool calls lost their live state. nativeChatTurnMembership now answers both questions from the turn record: each row's turn, and the live turn (the running root turn's anchor, else the newest user row, which is also all an unscoped host has). Desktop and mobile key liveness, the timing clock and the live status's row on it. * test(native-chat): a turn the provider opened keeps its own clock Pins that the local turn clock follows the live turn, so a wake after a settled turn does not restart that turn's clock when no host durations are recorded. * fix(native-chat): a running turn no message opened draws its status on no row Its live status belongs to the transcript-tail indicator alone. Once it settles, its duration draws at its first row as before; a running turn a message opened still draws on that message. * fix(native-chat): every copy of a row carries the main agent's own status History entries, sleep records and `worktree ps` rows carried a flattened top-level `outcome`, copied under different gates and without the main agent's clock. They now carry `mainAgent` (state, outcome, stateStartedAt), the type the live row already persists and sends, and every copy site takes it with `interrupted` through one function, `agentVerdictFields`. - The accessor reads `mainAgent` then the legacy flag; the mobile mirror matches it line for line. - Sleep records admit `mainAgent` with `normalizeMainAgentStatusField`, so a malformed value drops the field, never the record. - Mobile dates a main agent that failed under live subagents by its own clock, as desktop does, and its row equality compares `mainAgent`. - The activity feed reads a history entry's own `mainAgent` instead of rebuilding one; the sync key and history equality compare it. * test(native-chat): pin the worktree ps verdict across host and phone versions Pairs the real v1.4.212 host and phone row reader with this build: an old phone reads a new host's rows by `interrupted`, a new phone reads an old host's rows (no `mainAgent`) the same way, and a new phone reads a failure under live subagents as Failed, dated by `mainAgent.stateStartedAt`. The release checkout now carries the phone's self-contained row reader, and the lane runs when the `worktree ps` row producers change. * test(mobile): name the parity table's row for its role * test(native-chat): a roster of idle or finished children does not keep an agent awake The sweep reads owed background work through the shared child-work liveness that upstream's release clock adopted; a child that went idle or finished is not work the agent still owes. * fix(native-chat): a request that settles while the user is asked something notifies once The completion edge waited for an idle session, and a pending prompt (including a subagent's approval) is not idle. Structured chat has no other attention producer, so a main turn that finished while a subagent waited on the user sent nothing until the prompt was answered. The edge now waits only on owed work (a running turn or an unanswered send), which the projection reports even beneath a pending prompt. A request that settles with a prompt pending announces once; the renderer words it "needs input" from the host status mirror's `attention`, and answering the prompt keeps the same request identity, so it does not announce again. The wire shape is unchanged. * fix(orchestration): a task dispatched into a resting structured worker keeps it running The sweep's open-dispatch check read only the worker-start dispatch that owns the worker's terminal resource, so a task later dispatched to the same worker (orchestration dispatch --to, which writes a dispatch with no worker row) did not count: after thirty quiet minutes the worker was stopped while that task was open, and its coordinator read exited. Any unsettled dispatch addressed to the worker's process incarnation now counts, derived from the existing rows. * fix(native-chat): a command's wait ends when its child does The delivery loop waited for a /compact only on the adapter's compaction tracker, which learns of the child's end only on some exit paths: a Codex exit or close, and a Claude close, never reach it. The wait then never ended, so nothing queued behind the command was delivered again, Stop had no child to answer through, and the tracker's leftover entry refused the next /compact. Every way a child ends passes endProviderChild, so the host now offers a per-child end signal there. The loop races the tracker against it (the dead-generation settlement has already written the command's verdict), and on that end asks every adapter to release the command, so a later command runs and no later provider turn is claimed into the dead one. The adapters' own exit-time releases were unreachable (Codex) or covered one path of several (Claude), and are removed. The Codex RPC test harness moves to its own module so the exit can be driven through the real adapter's connection callback. * fix(native-chat): keep refusing sends during a command on an older host An older host's controller still refuses a send while a conversation command runs, so dropping the client's block turned every message typed during /compact into a 'not sent' row with Retry there. The block stays for hosts that do not run the command as a send-path turn, and goes only for those that do. The signal is one the client already holds: a host that runs /compact on the send path states a turn scope on every journal row it writes, the same fact turn membership uses to tell it from an older host. Both now read it from one predicate. On an empty conversation, or one whose rows all predate the upgrade, the signal is absent until the command's own entry streams in, so that brief window keeps the old local refusal; no capability or wire field is added. * docs(native-chat): comments stop describing the hold this PR removed Eight comments still justified orderings and teardown choices by a viewer or dispatch hold that pinned the provider child. Nothing holds any more; the orderings stand for the binding's redrive subscription and parked mail, and a chat's agent runs from a send until the idle sweep rests it. Comment-only. * fix(native-chat): the completion says when the user is being asked A request that settles while a prompt waits on the user was worded "needs input" from the renderer's status-feed mirror. Remote clients receive the status and completion streams over separate sockets, so they can arrive in either order and the wording could be wrong both ways. The host already knows at emit time, so the completion now carries an optional `awaitingUser: true` in that case and omits it otherwise. The renderer words the notification from that field alone and no longer reads the status mirror. Old clients ignore the field and word by outcome; old hosts never send it. * fix(native-chat): a restart offer keeps the start its own continuation made Whose start ended an offer was decided at read time, from whether the offer's continuation was still the queued message. Once the provider refused that continuation, the child it had started read as someone else's start, so the offer ended and its failure showed no Retry. The delivery loop now records which queued message a start is for on the in-memory child, and the child's end carries it; the offer counts a start as its own when that message is one of its continuations. * fix(native-chat): a rewound turn still names the message that opened it A Codex rewind rebuilds the epoch without submissions, so each sent message survives only under its provider key. The kept turn records still named the submission key, so each turn anchored on itself and its rows grouped apart from the message that opened it. The rewind now renames the turn's opener along with the message. * fix(native-chat): Stop ends only the command it names Stop on a command turn abandoned whatever compaction the session had pending, so a late Stop for an earlier /compact cancelled the one running now. The tracker now ends a command only when the Stop names its turn, and the cancel reply reports whether it did. * fix(native-chat): an agent gets a full idle window after its owed work ends The sweep measured quiet only from the last journal row, so once a subagent, command, monitor or dispatch that had outlived the window ended, the agent was stopped at the next tick. A child can read done before the lead's wake-up turn writes anything, and stopping in that gap loses the wake-up. The sweep now counts owed work it observes as activity, which gives the agent the full window afterwards, as the release clock it replaced did. * test(claude): the options-read fixture runs a live child The fixture marked its conversation running with a hasProviderChild field the session type does not have, so the read took the at-rest path and refused a session with no record. It now carries a child, which is what the read checks. * test(native-chat): host tests reach its collaborators through a typed seam The rest-test rig and three test files read the host's private members with Reflect.get and cast the result. The host now exposes one test-only accessor, collaboratorsForTests(), and the subscribers class a subscriberCountForTests() beside its existing retainedActivityCountForTests(), so the tests are checked against the real types and the casts are gone. * fix(worktree-status): a departed agent's failure yields to live work on the worktree card A retained failed agent has no expiry, so ranking it with a live failure pinned the card to Failed over other panes' live work. It now ranks below working, monitoring and permission, and above every finished outcome. * refactor(orchestration): one owner answers a structured worker's custody Routing, group addressing, worker-show and the idle sweep each composed their own reading of whether orchestration still holds a structured worker, so each new obligation or retirement state had to be added to every reader. structured-worker-custody now derives both answers from the worker-terminal list state coordinators see in worker-list: addressable is owned and not released, and owed work is an active custody or an unsettled task dispatched to the same incarnation. The owner's state is read through the remote dispatch attachment too, as the terminal transfer lookup already does. Behaviour is unchanged; a settled worker awaiting its coordinator still rests. * refactor(orchestration): owed work is an open dispatch on the worker's incarnation A supervised worker's own dispatch context stays open exactly while the worker is active, so the separate active-custody branch only repeated it. Owed work is now one fact, which also states the policy that a worker awaiting its coordinator's decision may rest, and both custody decisions are written once at the top of the module. * docs(agent-status): a departed agent's failure ranks below live work on the worktree card * fix(native-chat): a restart offer knows its continuations by a tag in their id The offer recorded each continuation id in a list on its capsule entry, capped at 16, and a running action's id in memory. Both could disagree with the journal: past the cap an old rejected continuation read as the chat moving on, and a crash during a retry restored the failure's older entry, which lacked the retry's id. Each continuation id now carries a tag derived from the offer (its teardown and chat), then the action's own part, so any continuation of this offer, queued or rejected, is recognised from the journal row and the marker alone. The persisted list, its cap and the in-memory action map are deleted; the agent-start withdrawal keeps an offer whose own continuation the start was for, read against the stored marker. * test(runtime): the legacy-worker reveal test judges its stale snapshot inside the wait The tui-idle probe reads through readTerminal, which now awaits the structured worker check before the PTY read, so the probe's snapshot request starts a microtask later. vi.waitFor missed it on its first check and polled again at 50 ms, the same moment the wait's own 50 ms timeout fired. The stale snapshot then resolved after the wait had already timed out, so the test passed without judging it, and the rejection landed before any handler was attached. Vitest reported that as an unhandled error and failed the shard. Polling every 1 ms sees the request within a few ms, so the snapshot is judged while the wait is still pending. * fix(native-chat): a message held behind /compact is drawn where it was handed over A message typed while /compact runs was drawn above the compaction's result, between itself and its own answer. The reducer kept every item at the sequence and timestamp of the row that created it, and a queued message is created at acceptance, long before the command it waits behind writes its result. The phone orders by that sequence and the desktop by that timestamp, so both put the message first. A queued message now takes its position from its handover row, the same row that already states its turn scope. Everything the agent did before the handover, a command it waited behind included, draws above it. This holds for every held message, not only /compact's, and needs no client change: every client, older builds included, reads the position the host publishes. A live batch already carries the item when its dispatch row lands, and history pages cut the reduced timeline by sequence, so paging stays contiguous. * fix(native-chat): a phone's send during /compact answers without waiting out the compaction A client that predates accepted-send replies, which is every phone build, has its send reply held until the host hands the message over. A message sent during /compact is not handed over until the compaction ends, so the phone's 15 s request timeout fired first and showed the message as unconfirmed. That wait now also ends once the message is queued behind a running command. This is read from the journal's running turn and needs no new state. Every other wait still ends at the handover: behind a starting child or an ordinary turn, and for restart resume, the command front door and orchestration, which keep the plain handover point. * perf(native-chat): a rewind places provider items with one pass over the merged rows A Codex rewind gives each provider item the old epoch never held the turn record for its provider turn. It found that record by scanning every merged row, restoring each row's body, once per provider item. That is quadratic, and it runs on the host's main thread up to the journal's 10,000-row cap, twice per rewind. A rewind record written before rows carried their scope holds no scope for any provider item, so it paid the full cost. The merge now indexes turn records by provider turn id once, keeping the first match as the scan did, and each provider item looks its record up. * fix(native-chat): a view never restarts a chat whose last start failed A Claude chat whose CLI exits during startup left one red row per start, and every time a view bound to it (the chat opening right after its create died, or the user switching back to it) the hold started the CLI again, so the same launch-failure row repeated. Only a send retries a failed start now, the same rule provider-exit recovery already applied; the rule lives in one predicate the hold, exit recovery and the delivery loop share. * fix(native-chat): a message waiting behind /compact is drawn after it until it is sent A message sent while /compact runs is placed where it was handed over. It was still drawn where it was accepted until then. /compact writes its result one step before the handover, so for that step the waiting message sat above the compaction's separator. A message the host accepted but has not handed over is not part of the conversation yet, so both clients now draw it after everything the agent has done. The shared projection moves it to the end, which is the order the phone draws. The desktop ranks it with the other not-yet-sent rows, after the streaming preview. At handover it takes its place from its handover row, which is also after the separator, so it never appears above the compaction it waited for. * fix(native-chat): the idle sweep reads owed work every tick Owed work counted as activity, but the sweep read it only once the idle window had elapsed, so it refreshed the clock at most once a window. Work that ended just before the next read left the agent to be stopped at that read, moments after the work ended, which is the gap the refresh was meant to cover. The sweep now reads owed work on every tick for a started agent, so the window always runs from the last tick that saw work owed. * fix(native-chat): a continuation handed to the agent stays sent The offer read its own continuation as not reaching the agent while its dispatch was pending, which also covered one already handed over and still unanswered. When the wait for that answer ended first, the failure it filed read as retryable, and a retry sent a second continuation to an agent that may have acted on the first. Only a continuation still queued, or rejected, is now read as unsent. * test(native-chat): start the child the loop waits on with an attach, not a second view A view no longer starts a child whose last start failed, so the R2 case that waits on a child started since the failure now gets that child from a client attach, the one non-send starter left. * fix(native-chat): settle a gone generation's turn wherever a conversation opens A send that opens a chat this process had not read yet (after a crash, from a phone or the CLI) went through the delivery open, which never settled what the dead generation left running; only the read restore and a successful acquire did. When the send's start then failed, the turn stayed running for every reader. The settlement now runs in the one journal open, at the crash boundary, for every opener except an acquisition, which settles from the evidence it read before its reserve; the read restore's separate step is gone. * test(native-chat): prove the next child's start settles the turn an earlier child left The R1 case lost its only settlement assertion when the latch it checked was deleted. It now seeds the running turn the earlier child left and asserts it ends at the exit's receipt, with the exit's row, before the message is handed to the new child. * test(native-chat): count a failed start's rows by row, not by text Comparing the set of texts passed when two different rows carried the same words, which is the duplicate the test exists to catch. * test(cross-version): load the phone row readers without mobile's toolchain Vite transforms a file against its nearest tsconfig, and mobile/tsconfig.json extends expo/tsconfig.base.json, which the root-only cross-version lane never installs. The worktree ps verdict suite imported the current phone row reader from mobile/ directly, so CI failed with TSConfckParseError before any test ran. The harness now imports a copy of the working-tree reader placed under the checkout cache, where the root tsconfig applies, as it already does for the release checkout's copy. Both readers are still the real files. * test(cross-version): keep the checkout path-guard message and justify the copy import's cast * fix(native-chat): a command ends only by its own provider answer or its child's end Stop no longer settles a conversation command. It interrupts it like any turn, and when the provider cannot take that (Codex has not opened the command's turn yet, or Claude refuses the interrupt) it stops the child, whose dead-generation settlement writes the verdict. The pending command now lives on the provider child's own session instead of an adapter-wide map keyed by session, so it dies with the child and nothing has to release it. Claude's /compact is sent under a uuid the slot records, and only a root result naming that input (or naming none) ends it; its outcome is read with the ordinary result reading, so a stopped /compact is a cancellation. * fix(native-chat): a command's settle answers its message before ending its turn The two writes are not one batch. Writing the message's answer first means a crash between them leaves a running command turn, which the stale-turn sweep already settles, instead of an ended turn whose message reads as in flight forever. The settle now writes only while the command turn is still running. * fix(native-chat): "Worked for" counts from the handover, not the send A message held behind /compact, or behind a cold start, used to count the wait as the agent's work, although its row is drawn at the handover. Every handed-over submission's turn, the command's own included, now starts at the handover row's instant, falling back to the send time for a host that recorded none. * test(native-chat): give the failed-start and stale-turn waits a loaded runner's budget * test(native-chat): the interrupted create's own retry continues again The merge of main's lease-latch fix replaced that test's retry of the interrupted create, under its own operation id, with a fresh start whose result nothing read. That fresh start passes with the released-reservation continuation deleted, so the case the fix exists for went untested. The retry and its assertion are main's again. * docs(native-chat): three comments that still had views starting agents A start with nothing queued now comes from a command, goal change or rewind; an interrupted compaction left alone would refuse every send, so no agent would ever start to finish it; and a current host raises the unattached read refusal only once quit began, with the attach window belonging to an older host. * test(native-chat): pin the open's and the send's start and row counts, however the view binds Opening a fresh chat whose starts fail makes one start and one row, with two views bound before or after the create's child died; one send makes one more of each. * fix(native-chat): a second Stop on a command ends its child; one compaction verdict for every provider A Stop's note now names itself in its key, so a later Stop on a command still running reads, from the journal, that the provider was already asked and never answered, and stops the child instead of interrupting again. Nothing is held in memory for it. Adds the rule both translators will read a compaction's end by: only a compaction the provider reported is a success; none after Orca's interrupt is a cancellation; anything else is a failure. A real Claude capture, pinned as a fixture, is why: a stopped /compact ends in the same success result as a finished one. * test(native-chat): a reader's open settles the turn a failed exit settlement left running An exit whose settlement write failed leaves its turn running in the open journal. PR 1's open now settles it, and this pins the two reads that reach it here: a reader reopening a chat the idle sweep closed, and a read that opens the chat before the restart restore reaches it. * test(native-chat): the view-start test's starting window outlasts two subscriptions on a loaded runner A subscription reads the conversation before it returns, so under load the two views took longer than the create child's 300 ms start, which then exited before the test checked that it had not. The child now takes a second to fail. * fix(native-chat): settle a gone generation's turn at every open but an acquisition's The journal open skipped the settlement whenever the lease read reserved or live, to leave an acquisition's own open to the acquisition. But a lease a crashed process left in recovery also reads live, until the next acquire resolves it. A send that opened such a chat, from a phone or the CLI after a crash on a host that could not prove the old owner gone, skipped the settlement; when its start then failed, the dead turn stayed running for every reader. The acquisition now says it is the opener, and every other open settles, whatever the lease still claims. * test(native-chat): hold the create's start open until the views bind The "view binds while the create is still starting" case gave the create a 300 ms head start and asserted the views bound before it died. On a loaded runner the holds took longer, the create's exit landed first, and the case failed its own precondition. The create's initialize now waits on a gate the test releases once the views are bound. * refactor(native-chat): the provider's translator ends a command's turn; the loop holds no command state A conversation command is now a turn of the provider child's own journal pipeline. The adapter-wide tracker, its promise and the loop's settle step are gone. - Codex: the translator claims the provider turn that carries the command, scopes its rows to the command's turn, and writes the command's end in the same batch that settles that turn. Codex's own compaction marker is the success row. - Claude: the command's turn is the translator's open turn until the result that answers the /compact input ends it. The command's own frames, such as the continuation summary, its echo and "Compaction canceled.", draw nothing. - Both read the end with the one compaction rule: success needs the provider's report of the compaction; none after Orca's interrupt is a cancellation. - The message resolves at the provider's receipt, as any send does: the Codex ack, or the Claude slash-command waiter on its result. The host writes a command's end only when the provider never took it. - The delivery loop stops while a command's turn runs, and every journal commit re-wakes it through the session's serialize, so an end that lands while a step decides to stop is never lost. A child that ends first is settled with it. * test(native-chat): pin a command's end to real /compact frames and to each path it threads The captured /compact frames drive the Claude translator's command turn: a finished compaction ends as a success with only the separator drawn; a stopped one ends as a cancellation with no failure row, and the next send answers in its own turn; a result naming another input ends nothing. The command's end is checked at each point the ordinary result path threads through: the reopen latch after a failure, the settling of a child still working, the context facts the result reports, and the provider's own error row. On the host: a message held behind a command is handed over when the command ends just as the loop stops for it, a refused command settles as a failure and the loop moves on, and a Claude child that exits mid-command settles the command and hands what waited to a fresh child. * test(native-chat): tests merged from the base state which turn their rows belong to * refactor(native-chat): drop the child-end waiter nothing waits on A command no longer waits for its child here: its turn ends from the provider's frames or from that child's settlement, and the delivery loop is woken by the commit. The waiter and its test were left from the earlier shape. * fix(native-chat): a command holds the queue only while its child runs it The delivery loop stopped whenever the journal showed a command's turn running. When the command's child ended and its settlement could not be written, that turn stayed running with no child to end it, and the loop's gate kept it from ever starting the next child, which is what settles a gone generation's leftovers. Every later send was held for good, and Stop had no child to end. The gate now holds only while the conversation has a child: with none, the command belongs to a gone generation, and the loop's start settles it like any turn a dead child left running. * fix(native-chat): a Claude /compact succeeds only on its compaction boundary The command's evidence counted Claude's `compact_result: 'success'` status as the compaction done. That status comes before the boundary that replaces the history, so a Stop landing between the two read as a finished compaction even though no boundary was ever written. Only the boundary now counts, as the rule for both providers states; the capture's finished compaction carries one, so it still reads as a success. * fix(native-chat): a Claude child's exit says why the turn it ended stopped When a Claude child exited mid-/compact, the command showed "Worked for 0s" and no reason. The child's translator ends its open turn the moment the exit is reported, stamped with the exit's instant, so by the time the exit settlement ran nothing was running. The settlement recognises a turn the exit already ended by that same instant, but the Claude lifecycle event dropped it on the way to the host, which then used its own clock, matched nothing, and wrote no row. When the clocks did agree, the row was scoped to the running turn, of which there was none, so it landed outside the turn it explained. The exit's instant now reaches the host, and the exit row belongs to the turn the exit ended: still running, or ended by the translator at that instant. * fix(native-chat): a message waiting behind /compact draws below its live activity A message sent while /compact runs waits on the host until the command ends. Both clients moved it to the end of the transcript rows, but the running turn's live activity line ("Compacting the conversation") draws after every row, so the waiting message sat between the command and its own live status. A row that is queued, and not what the live turn is for, now draws after that live activity: on desktop outside the transcript window, below the activity line; on the phone in the list footer, below the live status. A message whose own start is pending still draws above the activity that start reports. * fix(native-chat): only a running command holds a message below its live activity A message is accepted, then handed over a moment later, and in between it reads as waiting. Every message waiting behind a live tur…
…agent does, so no stray effort picker appears (stablyai#24267) * fix(native-chat): a resting chat lists its current model as a live child does, with no phantom effort A chat at rest (no live child) answered its options from the host's model catalog alone. When the chat's model is one the catalog does not list, the answer left it out, the client filled the gap from its static seed, and the composer showed an effort control ("Medium") that the live child never offers; the control vanished again once the child was back. The live Claude and Codex answers and the resting answer now build their model list through one function: the catalog's rows, plus the current model when the catalog does not list it, with no effort levels. Live and rest can no longer disagree about it. * fix(native-chat): a resting chat with no catalog yet lists what a live child would, so a listed model keeps its effort When the host has no model catalog for the account yet (the first read before its probe returns, a failing probe, an account-home lookup that failed, or a WSL-pinned chat no live child has written one for), the resting answer listed only the saved model, as unlisted, and a listed model such as opus lost its effort control. It now rests on the list a running child falls back to: Claude's built-in models, then the shared function. Codex lists nothing without its catalog, so the resting answer leaves the list to the client's own defaults, as a failed live read does. * fix(native-chat): a resting chat with no pick and no catalog names no model, as before With no catalog, the built-in list's default model is a guess, not the account's: naming it moved a chat the running agent reported on Opus to Sonnet. Only a real listing names the default now; otherwise the answer names none and the client keeps the model the agent last reported. The comment on Codex's no-catalog answer now says what it does: the client fills the current model from its own defaults, unchanged from before.
…tablyai#24088) * feat(persistence): run profile backups in the worker whenever its entry is bundled * refactor(orcad): make profile and native preflight runtime-neutral The profile preflight parser now takes the expected runtime identity from the caller (shipped callers pass the pinned Bun identity), and the native preflight is renamed to orcad-runtime-native-preflight with neutral wording. * test(persistence): skip plain-Node backup selection tests in the Bun profile suite --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…ase (stablyai#24089) Lands daemon-protocol-facts.mjs from the Windows update diagnostic branch with a stricter parser, and adds check-daemon-protocol-crossing.mjs (rule R1): the working tree must attach the newest release tag's daemon. Rollback crossing is reported only. Runs in the cross-version-wire job, which already has full tags; tag selection moves to config/scripts/stable-release-tags.mjs so both use one rule. Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…own (stablyai#24071) The test re-arms a setImmediate tick while the copy runs and stopped it with a flag. A tick already queued when the import resolved still ran, and when the afterEach teardown closed the database first it threw journal_closed as an unhandled error, failing a CI shard with every test green. Clear the queued tick instead.
…es the conversation (stablyai#24235) * fix(claude): a Stop ends Claude's process, and the next message resumes the conversation Claude's Stop now ends its child after the interrupt, whatever Claude answered, once the stopped turn ends or a 3 s grace runs out. The grace lets Claude's own result move the resume point past that turn, so the next send resumes after it. Background work ends with the child. Codex keeps its child. * test(native-chat): the router answers stopEndsSession for the session's live owner * test(native-chat): type the Stop test's close mock as the adapter's optional method * fix(native-chat): a Claude Stop answers on its interrupt, and the next step on the chat's lane ends the child The Stop now answers as soon as the interrupt step finishes. Ending Claude's process runs as a second task on the session's serialized lane, queued in the same tick as the Stop, so a message sent meanwhile reaches only the resumed child. That step waits for the stopped turn to end (its result, the CLI's idle, the child's exit or its close), then ends the child; a failure there is reported and never fails the Stop, and the wind-down it leaves owed is retried. The interrupt's answer and the turn's end share one 3 s grace counted from when the interrupt goes out, so an interrupt Claude never answers ends the child in about 3 s instead of after the 30 s control deadline. The wait is derived per call from Claude's open turn: the armed latch and the close's user-stop branch are gone, and the close keeps its whole deadline for proving the exit. A Stop naming a turn that has since ended (the phone names the turn it last saw) now ends the child too when Claude took it, as it does when Claude interrupts a handed-over follow-up whose turn has not opened. * test(native-chat): pin a Claude Stop's race, failure and timing cases on the shipping adapter - Nothing queued while the Stop's first step waits on the interrupt runs before the child ends. - A message sent with the pre-Stop fence during the Stop is accepted with no notice, and reaches only the resumed child, after the old child's close. - An interrupt Claude never answers ends the child within the grace. - A close that cannot prove the exit leaves the Stop answered with its row; the failure goes to the host's error hook. - A Stop naming the turn that just ended ends the child when Claude interrupts the handed-over follow-up. - A second Stop pressed while the first ends the child stays quiet. * fix(native-chat): a Stop naming an ended turn ends Claude's child when its interrupt fails or goes unanswered The phone names the turn it last saw. When Claude interrupted a handed-over follow-up for that Stop and the answer failed or never came, the child stayed. Only a provider that answers it did not take the Stop keeps the child now. Also pins a queue-if-active send made while the Stop ends the child. * fix(native-chat): a Claude card's Cancel denies an approval and stops on a question, never a bare interrupt A permission card's Cancel interrupted the turn and kept Claude running, the old Stop on a second control: a refused interrupt let the turn run on and background work survived. Now the host asks the provider how a card's Cancel is answered. For Claude, an approval's Cancel sends the same reply as its Deny option, so the turn goes on without that tool; a question's Cancel runs the chat's Stop, which ends the child, and the next send resumes the conversation. A provider that gives no answer keeps today's path, so Codex is unchanged. The host decides, so desktops and phones of any version keep sending the same cancel and get the new behaviour; nothing on the wire changes. With no Claude caller left, the prompt-cancel interrupt is gone: the bound claim, the cancellation observation, the admission of the prompt's cancellation, the prompt's turn binding, and withdrawing a refused stop (every Claude interrupt now precedes the child's end, so the recorded stop stands). * fix(native-chat): a Claude approval card's Stop option is the chat's Stop The approval card's "Stop" option answered Claude with a deny that interrupts the turn and kept Claude running, the last control on a Claude chat that interrupted without ending the child. The host now routes it, like the card's own Cancel, through the provider: for Claude that option is the chat's Stop. It runs the same Stop body as the Stop button, in the same order (withdraw, the Stop takes effect, interrupt), and the step that ends the child is queued in the same synchronous call. The body now lives in one place, structured-agent-session-chat-stop.ts, shared by the Stop button, a question card's Cancel and this option. The host decides, so an old card or client that sends option `cancel` gets the real Stop; nothing on the wire changes. The interrupting deny reply is gone. * test(native-chat): the option-route test's answer carries a whole journal resolution * fix(native-chat): Claude cards drop their Stop option, and a plan card's Cancel asks Claude to wait - Approval and plan cards no longer offer "Stop". Neither reference offers a Stop while an approval is up: the user denies, then stops. An older card's or client's `cancel` answer is a plain deny, and the respond path is main's again. - A plan card's X / Esc no longer answers "Keep planning", which sent Claude straight back to revise and re-propose. It dismisses the plan: a deny that tells Claude to end its turn and wait for the user. No interrupt; the child stays. A tool approval's Cancel stays the Deny reply, and a question's Cancel stays the chat's Stop. - The provider's routing takes the pending card itself (`routePromptCancel({ sessionId, prompt })`), so a plan is told from a tool approval, instead of a kind plus an optional option id that meant Cancel when absent. A routed answer may be one the card does not offer, like the dismissal. - The chat's Stop is reachable only through `mutateWithChatStop`, which owns the mutation call and queues the step that ends the child in the same call, so no caller can run the Stop and forget the child's end. * refactor(native-chat): cancelPlan no longer takes a stopChild nothing passes The Stop's own body ends the child; the plan's default run serves only a card's interrupt and the background-task stop, which never end it. * fix(native-chat): a question card's Cancel settles the card in the Stop's first step, and answers Claude when nothing stops A question card's Cancel runs the chat's Stop. The card stayed pending until the child ended (up to about 3 s), so it could still be pressed and the second press was refused once the chat rested. When the Stop found nothing in flight (Claude asking after its main turn ended), the card stayed pending for good and Claude's request went unanswered; the phone froze the card with every button disabled. Now the card is recorded as cancelled by the user in the Stop's own step, with the receipt "Cancelled on <device>". A Stop that ends the session leaves Claude's request to end with the child, so no reply races the interrupt; one that ends nothing declines the request itself. The provider forgets the card once the host records it, so neither Claude's own cancel nor the child's end writes over the user's cancel. The card's Stop stops whatever the chat has in flight, like the Stop button, instead of a turn the card names that may have ended. The same dismissal, a new provider member beside the cancel route, now answers a plan card's Cancel: recorded as cancelled by the user, and Claude told to end its turn and wait. That replaces resolving it as an option the card does not offer. * test(native-chat): the question-card Stop test has Claude cancel its held request, as it does once interrupted * fix(native-chat): a Claude Stop before the echo reads Interrupted, and a question card's Stop stays in its own turn - A Stop pressed after Send but before Claude echoed it read as a normal finish ("Worked for 0s", a green done check): with no turn row to wait on, the child was ended at once, and the echo that would have opened the stopped turn landed on a retired send. The wait before the child ends is now keyed on what Claude has in flight (an open turn or a send it has not answered), not on a journal turn id, within the same 3 s from the interrupt and woken by the same settles; a settle that leaves something in flight waits on. The echo opens the turn, the aborted result ends it Interrupted. A Claude that says nothing is ended when the grace runs out, the unanswered send doubt as before. - A question card's Cancel takes the chat's Stop only when the card belongs to the turn running now, judged after draining the provider's lifecycle. A card a finished turn raised, such as a background agent's, is dismissed instead: Claude's request is declined and nothing stops. - Claude forgets a dismissed card before the host records it, so Claude's own cancel landing during that write cannot replace the user's "Cancelled on <device>". * test(native-chat): Stop tests read background work from the host's child records, and guard the optional dismissal Main's child records replaced the adapter's background-task callback: the background-work test now reads the host's child record (live before the Stop, settled after the child ends) and the session's agent status (done, Interrupted, nothing left for Monitoring). * fix(native-chat): a tool card's Cancel reads Cancelled, and a Stop's background work reads stopped - A tool approval card's X / Esc already sent Claude the plain deny, but the card read "Deny · Answered on <device>", the same as pressing Deny. It is now a dismissal like a plan card's: recorded as cancelled by the user, with the same deny reply, no interrupt and the child kept. Every card's Cancel now routes to a dismissal or the chat's Stop, so the option route is gone. - When a Stop ended Claude's child, a background agent or shell it was running settled as an unknown ending (a neutral dot), while the same task's own stop reads Interrupted. A close Orca asks for (a Stop, a rest, a quit) now ends what still runs as stopped; only a session that dies on its own leaves the ending unknown. The close reads no stop cause. A rest never meets live child work: the idle sweep keeps a chat with any. - If the host fails to record a dismissed card, Claude gets the card back, and a withdrawal Claude made meanwhile closes it, as before the host took it. * refactor(native-chat): the session mutation path gets its own module, breaking the chat-stop import cycle chat-stop.ts imported mutateStructuredAgentSession from host-mutations.ts, which imports mutateWithChatStop back. The mutation context and the one admit-then-serialize path now live in structured-agent-session-mutation-context.ts, which both import; host-mutations re-exports the context type for its other readers. * fix(claude): a close that saw a descendant survive leaves background work's ending unknown A Stop, rest or quit ended still-live background tasks as stopped even when the close found Claude's root gone but a descendant still running. Only a close that proved the whole tree gone stops them now; otherwise they settle unknown, as for an exit of the session's own. The stopped ending is marked as Orca's, so a frame of the child's own still replaces it. * docs(native-chat): comments describe the Stop as it now works What re-drives a failed child end, when the child-work decoder stops what is live, what a Stop's wait re-reads on a result, and why a card's Stop is unnamed.
…tablyai#24108) * ci(daemon): gate PRs on daemon protocol crossing from the newest release Lands daemon-protocol-facts.mjs from the Windows update diagnostic branch with a stricter parser, and adds check-daemon-protocol-crossing.mjs (rule R1): the working tree must attach the newest release tag's daemon. Rollback crossing is reported only. Runs in the cross-version-wire job, which already has full tags; tag selection moves to config/scripts/stable-release-tags.mjs so both use one rule. * feat(persistence): run profile backups in the worker whenever its entry is bundled * refactor(orcad): make profile and native preflight runtime-neutral The profile preflight parser now takes the expected runtime identity from the caller (shipped callers pass the pinned Bun identity), and the native preflight is renamed to orcad-runtime-native-preflight with neutral wording. * feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check Add src/shared/node-runtime-pin.ts (NODE_RUNTIME_PIN, SERVER_TARGETS, NODE_RUNTIME_ASSETS for all 8 server targets plus the headers tarball), generated by config/scripts/update-node-runtime-pin.mjs from the nodejs.org and unofficial-builds SHASUMS. check-node-runtime-pin.mjs verifies, with no network, that the pin tracks the locked Electron, matches engines.node's major, and covers exactly SERVER_TARGETS; it runs in the static analysis job. ORCAD_BUN_TARGETS consumers now read SERVER_TARGETS so there is one target list; orcad's Bun runtime and build output are unchanged. * test(persistence): skip plain-Node backup selection tests in the Bun profile suite * fix(runtime): reject a pinned archive that belongs to another target * ci(daemon): fail PRs that swap a runtime launcher and bump the daemon protocol D7.1 R3: hosting orcad or the daemon on another runtime is not a protocol change, so one PR must not do both. The launcher file list lives in the check script; the allow-runtime-launcher-protocol-bump label overrides it. * feat(orcad): select pinned-Node slots by a .runtime-node marker D7.1 R5: a Node slot names its shared runtimes/node-<sha256>/node through .runtime-node instead of .build-target, so Bun-era clients read it as a legacy slot rather than exiting 78 on a missing bun-runtime. Nothing builds the marker yet. * fix(runtime): load the Node pin without the typeless-module warning check-node-runtime-pin.mjs now requires the pin and takes nodeDistArchiveName from its own module, so it no longer loads the update script's build graph. * fix(orcad): resolve Node slots to the design's runtimes/node-<sha>/bin/node layout --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
) * ci(daemon): gate PRs on daemon protocol crossing from the newest release Lands daemon-protocol-facts.mjs from the Windows update diagnostic branch with a stricter parser, and adds check-daemon-protocol-crossing.mjs (rule R1): the working tree must attach the newest release tag's daemon. Rollback crossing is reported only. Runs in the cross-version-wire job, which already has full tags; tag selection moves to config/scripts/stable-release-tags.mjs so both use one rule. * feat(persistence): run profile backups in the worker whenever its entry is bundled * refactor(orcad): make profile and native preflight runtime-neutral The profile preflight parser now takes the expected runtime identity from the caller (shipped callers pass the pinned Bun identity), and the native preflight is renamed to orcad-runtime-native-preflight with neutral wording. * feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check Add src/shared/node-runtime-pin.ts (NODE_RUNTIME_PIN, SERVER_TARGETS, NODE_RUNTIME_ASSETS for all 8 server targets plus the headers tarball), generated by config/scripts/update-node-runtime-pin.mjs from the nodejs.org and unofficial-builds SHASUMS. check-node-runtime-pin.mjs verifies, with no network, that the pin tracks the locked Electron, matches engines.node's major, and covers exactly SERVER_TARGETS; it runs in the static analysis job. ORCAD_BUN_TARGETS consumers now read SERVER_TARGETS so there is one target list; orcad's Bun runtime and build output are unchanged. * test(persistence): skip plain-Node backup selection tests in the Bun profile suite * fix(runtime): reject a pinned archive that belongs to another target * ci(daemon): fail PRs that swap a runtime launcher and bump the daemon protocol D7.1 R3: hosting orcad or the daemon on another runtime is not a protocol change, so one PR must not do both. The launcher file list lives in the check script; the allow-runtime-launcher-protocol-bump label overrides it. * feat(orcad): select pinned-Node slots by a .runtime-node marker D7.1 R5: a Node slot names its shared runtimes/node-<sha256>/node through .runtime-node instead of .build-target, so Bun-era clients read it as a legacy slot rather than exiting 78 on a missing bun-runtime. Nothing builds the marker yet. * fix(runtime): load the Node pin without the typeless-module warning check-node-runtime-pin.mjs now requires the pin and takes nodeDistArchiveName from its own module, so it no longer loads the update script's build graph. * fix(orcad): resolve Node slots to the design's runtimes/node-<sha>/bin/node layout * feat(orcad): 8-slot node-pty prebuilds against the pinned Node headers at N-API 8 - build-orcad-prebuilds.mjs adds win32-x64/arm64 (conpty.node, the vendored conpty.dll/OpenConsole.exe, upstream's N-API conpty_console_list.node), compiles in a scratch copy against the hash-verified pinned headers (node.lib pinned per Windows arch) with NAPI_VERSION=8, rejects post-8 node_api_* imports, and writes a schema 2 manifest with per-file sha256, N-API level and the glibc need. - --require-slots [slots] verifies files against hashes; --smoke loads the slot under the pinned Node and spawns a PTY; --print-slot names the host slot. - The slot installer gates on N-API, libc, arch, glibc and file hashes instead of the exact NODE_MODULE_VERSION, and installs nested files (conpty/). - bun-profile-tests.yml builds, verifies and smokes each runner's slot. * fix(orcad): scope node-pty's glibc .symver pins to glibc on musl prebuild slots musl's unversioned libc cannot satisfy openpty@GLIBC_* references at link time, so the Alpine slot compile would fail. Pin the staged pty.cc guard to __GLIBC__ and assert both musl transforms against the installed patch. * feat(orcad): run orcad on the pinned Node instead of Bun A packaged orcad slot now references the pinned Node 24.21.0 by its executableSha256 (`.runtime-node`, `.server-target`) instead of carrying bun-runtime, and ships node-pty from the slot's prebuild, only its own ripgrep, and no Windows Bun PTY gate. The runtime lives beside the slots at runtimes/node-<sha>/bin/node (node.exe on Windows, upstream name). - build:orcad (build-orcad-node.mjs) builds the host slot's prebuild when missing and places the pinned runtime; the template is schema 3 with per-target files. - handoffToBundledOrcad() resolves the slot's runtime reference and checks process.versions.node against the pin; a host Node >= 18 still hands off. Startup preflight keys on running as that runtime; callers expect 'node'. - orcad and its daemon use node-pty (ConPTY + windows-pty-job on Windows); the Bun PTY sources, gate entry and canUseBunPty branches are removed. - SSH deploy uploads the official archive once per pin, extracts and hash-checks it on the host, and self-tests it before publishing. Bun slots stay launchable for rollback; Node slots never use host Node. - The runtime materializer is generic over pinned assets; the Bun wrapper remains only for the OpenCode vault reader (design Phase 2). - Cross-runtime test: a profile DB written by Bun 1.4.2 (WAL left by SIGKILL) opens and backs up under the pinned Node, and the reverse. No daemon PROTOCOL_VERSION change (design D7.1 R3). * docs(ci): name the headless lanes after the pinned Node, drop Bun shard timings Design D10: ci-demand-rollout.md and ci-runner-efficiency.md follow the bun-profile-tests.yml -> node-server-tests.yml rename; shard timings drop the deleted Bun PTY tests and follow the renamed ones. * chore(ci): count the runtime archive download as a runtime launcher path * fix(orcad): pin the macOS C++ standard for node-pty prebuilds The official Node headers' config.gypi sets clang: 0, so common.gypi skips its gnu++20 xcode_settings and Apple clang 15 (macos-14 runners) compiles node-addon-api as C++98. * fix(orcad): resolve the preflight's slot through realpath, as the handoff does A symlinked orcad.js handed off to its real slot's pinned Node, but the startup and profile preflights read the symlink's directory, found no runtime marker there, and silently skipped the readiness check. * refactor(ssh): drop materializeCachedNodeRuntime, which nothing calls Deploys upload the verified official archive (design D5); no client path needs an extracted Node executable cached by digest. * test(orcad): gate the Bun-to-Node upgrade and Node-to-Bun rollback with live terminals Design D7.1 R1/R3/R4 and D7.2. The last Bun orcad and this checkout's Node slot are installed side by side under ~/.orca-remote, launched and stopped with the client's own deploy commands, and share one data root. Each direction proves the incoming orcad adopts the outgoing runtime's daemon (same PID, same shell, output continues), opens its profile database and backs it up with its own shipped worker, and that GC keeps the slot the live daemon was forked from. The node-server Linux lanes provide Bun 1.4.2 and build that Bun orcad from main, and run with --cross-runtime. --artifact and --cross-runtime now make their tests fail on a missing input instead of skipping. * ci(node-server): pin node:24.21.0-alpine by its multi-arch index digest * test(ssh): name the runtime archive fixture after its role * test(node-server): load node-pty from the packaged slot in artifact runs The node-server lane installs dependencies without building node-pty, and Linux has no upstream prebuild, so the real-PTY failed-I/O teardown test (picked up by the pty-subprocess selector) could not load pty.node. In --artifact runs, alias node-pty to out/orcad's shipped slot so the test exercises the addon orcad actually runs under the pinned Node. * fix(orcad): let the Windows profile preflight exit after its PTY probe On Windows, node-pty keeps the conout worker thread and pseudoconsole alive until kill(), even after the shell exits. The PTY health probe never killed a cleanly exited probe, so the packaged preflight printed its readiness line and then hung until the build's 30s timeout, reported with an empty stderr. - The probe kills its PTY on Windows after exit and uses the bundled ConPTY the daemon spawns with. - The preflight exits once stdout is flushed; its owner reads to EOF. - Preflight failures now report code, signal, timeout, stdout and stderr. * test(node-server): load the slot's node-pty in the real-PTY test, not by alias A vite alias redirected only ESM imports of node-pty; windows-pty-job and local-pty-utils resolve it through require, so Windows loaded two conpty.node copies and the Git Bash job-membership proof read an empty job. The failed-I/O teardown test now loads node-pty through a fixture that picks the packaged slot in artifact lanes. The pty-subprocess selector was a prefix that also pulled in its POSIX-host sibling unit tests, which pr.yml runs and which were never qualified on Windows. Select the directory plus the two sibling files that belong here. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…ly (stablyai#24320) * fix(relay): gate the Asia canary on region fallbacks against a pre-canary baseline Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): gate the Asia canary on region fallbacks against a pre-canary baseline Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
…promotion (stablyai#24318) Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
…move Bun (stablyai#24128) * feat(ai-vault): read OpenCode history with the pinned Node instead of Bun SSH hosts whose Node lacks node:sqlite (or its backup(), which 22.13-22.15 omit) now get the pinned Node in the shared ~/.orca-remote/runtimes/node-<sha> store orcad uses: POSIX hosts receive the official archive and extract and hash-verify it on the host; Windows hosts receive the verified node.exe the client extracted, promoted by host Node with the same hash check. WSL distros use the same layout and checks under ~/.cache/orca/runtimes/. The Bun release pin table and its materializer are deleted. Old relays keep reading their vault-sqlite/<sha>/bun references; nothing deletes those files. An unconfirmed runtime upload now keeps its stage instead of removing it. * refactor(sqlite): drop the Bun SQLite adapter; node:sqlite is the only backend Nothing outside Electron runs on Bun any more (design D4), so SyncDatabase loses its Bun branch, and bun-sqlite-database, bun-sqlite-statement and bun-readonly-wal go, with the relay's bun:sqlite external. The profile-state backup worker admits Electron or an entry that exists, and startup errors name the pinned Node. The D7 cross-runtime gate still runs Bun 1.4.2, now reaching Bun's SQLite through its node:sqlite. * test(native-chat): drop the Bun SQLite driver case now that node:sqlite is the only backend --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
… store GC (stablyai#24130) * fix(ssh): relay version GC deletes only on an exited verdict and keeps the previous build The relay records .relay-pid in its version dir once it owns its socket. GC calls a relay version dir exited only when that PID is provably dead and every relay-*.sock refuses a connection; a dir without a PID file keeps the test -S rule. The most recently completed other relay build is pinned like orcad's rollback target. Design D5 GC liveness. * feat(ssh): collect the shared runtimes/ Node store and give it its own owner runtimes/ gets its own owner in the install model, so no version-dir GC (new or old clients, whose listings are prefix-scoped) can list or delete it. A store pass removes node-<sha> only when no retained dir references it, it is neither a current pin nor the newest other verified runtime, and a ps or /proc check ran and found no process using it. Legacy relay-*/orcad-* dirs are read for references and reported as diagnostics only (design D10 two-step). Wired behind orcad GC's nodeRuntimePins. * fix(ssh): runtime store process check holds runtimes reached through a symlinked home /proc exe resolves symlinks and argv keeps whatever spelling launched the runtime, so filtering on the exact $root path missed in-use runtimes on hosts like /home -> /var/home. Filter on the store segment instead; the parser already attributes holds root-agnostically. * test(ssh): wait for the holder process to spawn instead of a fixed delay --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…compat slot (stablyai#24134) * build(orcad): build server glibc slots on glibc 2.28 and add the glibc 2.17 compat slot Design D6: the default linux-{x64,arm64}-glibc node-pty slots now build in manylinux_2_28 (digest-pinned) and are gated at glibc 2.28 / GLIBCXX_3.4.25 through a floor profile on verify-linux-glibc-floor.cjs; the desktop keeps its Ubuntu 20.04 (2.31) default. Adds the opt-in linux-x64-glibc217 compat target: NODE_RUNTIME_COMPAT_ASSETS pins the unofficial glibc-217 Node (update/check pin scripts cover it, outside SERVER_TARGETS), and a new CI lane builds the compat slot in manylinux2014 with static libstdc++, gates it at glibc 2.17 with no shared C++ runtime in DT_NEEDED, and smokes it under the glibc-217 Node. * refactor(node-runtime-pin): route compat lookups through isCompatServerTarget; keep the glibc doc's slot-name paragraph intact --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…tablyai#24129) * feat(relay): runtime self-test flag and informational runtime on handshake-ok relay.js --orca-runtime-selftest <nonce> dlopens pty.node, opens and closes a PTY, and prints one JSON line (nonce, node, napi, glibcVersionRuntime) for the client to classify before it launches a daemon on a runtime (design D5). handshake-ok gains an optional runtime {kind, version}; bridge and daemon already match exactly on version, so it is informational only (D8.1). * feat(ssh): opt-in pinned-Node relay with prebuilt addons (D5, D6 rung A, D8.1) SshTarget.remoteRuntime (legacy | pinned-node, default legacy; env ORCA_SSH_REMOTE_RUNTIME for development) selects the runtime. On POSIX hosts the pinned path resolves the target with its glibc major.minor, ensures ~/.orca-remote/runtimes/node-<sha>/bin/node, uploads the relay bundle plus the target's node-pty slot and @parcel/watcher from the orcad artifact (no npm or node-gyp on the host), writes .runtime-ref-node-<sha>, and folds the runtime and addon digests into the relay version so pinned and host-Node builds never share a dir or socket. A 30 s self-test (node --version, then the relay self-test) gates .install-complete. Timeouts and lost channels are unverifiable and never step down; noexec, missing_lib, libc_floor, illegal_instruction and wrong_libc refusals fall back to the untouched host-Node path with a logged reason, remembered for the session. * fix(ssh): only an answered libc probe steps the pinned relay down A lost channel during target detection says nothing about the host; descending would launch a host-Node daemon beside a running pinned one and strand its sessions. * test(ssh): mark the mocked SSH connection casts in the pinned relay tests * test(ssh): resolve the pinned runtime mock to an executable path --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…-agent-session-adapter.ts so main's lint passes (stablyai#24323) structured-agent-session-adapter.ts reached 301 lines once two changes on main met, one over the 300-line limit, so oxlint fails on main. The file is the adapter contract: its types, its interface and the typed failure verdicts. The only logic in it read what a cleanup or a stop proved about the provider child's exit: rethrowing a failed acquisition with the verdict its cleanup reached, and reading whether a stop left the provider root gone. That logic moves unchanged to structured-agent-session-provider-exit-proof.ts, which imports the verdicts from the adapter; nothing imports back. Its six importers now import from the new module. No behavior change.
…setting (stablyai#24133) * feat(ssh): complete the relay runtime fallback ladder (D6 rungs B slot, C, D) Rung C runs the relay on the host's Node >= 18 with Orca's prebuilt N-API addons and no npm (addon-only probe mode). Rung B is a data-driven slot chosen only when a compat runtime is listed. Rung D fails the connect with a classified reason carried as a TerminalUnavailableCause. The ladder steps down only on classified refusals; unanswered probes throw. The rung decision is persisted per host keyed by (glibc, runtime hash, Orca major), and ssh_remote_runtime_resolved reports it once per host per session. * feat(settings): SSH host runtime choice (Auto | Orca-managed Node | Host Node) * docs(telemetry): describe ssh_remote_runtime_resolved * fix(ssh): let a passing rung C disprove a remembered noexec; allow glibc-less compat runtimes A remembered rung A noexec was re-persisted even after rung C self-tested addons from the same ~/.orca-remote tree, so rung A stayed skipped until the key changed. Rung B's evaluator also could never match a musl compat runtime. * test(ssh): import node:fs once in the host-node addon test --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…lly receives (stablyai#24343) The parent workflow collapses canary-apply and batch-apply into the job mode apply, so the headroom step's canary-apply/batch-apply condition never held and the gate was skipped on every real roll. Run it wherever the drain runs (apply, rollback before its restart) and in read-only verify; skip only a resumed rollback, which drains nothing. A new workflow-shape test fails on any job step comparing against a mode the parent cannot pass, and on a drain that can run without the headroom check. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
* feat(ssh): pinned-Node relay on Windows SSH hosts (D5 Windows, D2) Windows hosts opted into remoteRuntime 'pinned-node' now get the same rung A relay POSIX hosts do, instead of an early host-Node fallback. - Runtime store: the official node-v24.21.0-win-<arch>.zip is uploaded to a stage under %USERPROFILE%\.orca-remote\runtimes, verified against the pinned archive hash, node.exe extracted with System32 tar.exe (Expand-Archive fallback), hashed with Get-FileHash, run once, and published with node.exe + .verified by one Directory.Move. One powershell.exe per phase via the existing powerShellCommand helper; the probe also creates the stage. No new -EncodedCommand site, no -ExecutionPolicy, no Add-Type. node.exe keeps its real name at runtimes\node-<sha>\node.exe. - Bytes that change or vanish after Orca wrote and verified them are reported as ORCA_NODE_RUNTIME_SECURITY_MODIFIED and become a remembered 'security_software' refusal (fallback to the host-Node relay); application control blocks classify as 'noexec'. - Addons: the win32 slot's conpty.node, conpty_console_list.node, conpty\conpty.dll + OpenConsole.exe, watcher and windows-process-tree.node ride with the relay; the orcad template now carries the win32 targets. - Self-test on Windows is one powershell.exe running relay.js on node.exe; the report must name the pinned Node. The relay self-test loads conpty.node and opens a PTY with useConptyDll, and reports a missing bundled ConPTY file as a load failure. A pinned relay's terminals use the bundled ConPTY too; host-Node relays are unchanged. - describeRelayRuntime recognizes the Windows store layout. * fix(ssh): skip the redundant stage-cleanup powershell.exe after a Windows runtime promote The promote script already removes its stage on every path, so the client-side cleanup only runs when promote never returned (upload failure, abort, timeout). * test(ssh): expect the ladder's remembered flag and pin check on Windows pinned relays --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…ack (stablyai#24136) * feat(ssh): run runtime-store GC after a pinned relay launch, under a store lock (D5) The pinned-Node relay deploy now collects runtimes/ after a successful launch, keeping the pin it runs. Promotion in ensureRemoteOrcadNodeRuntime and GC deletion both hold runtimes/.store-lock (install-lock primitives, 20-minute stale rule); GC only tries the lock and skips when busy. A cold pinned install re-checks its runtime under the lock once the relay ref is visible, closing the ensure-then-ref window. GC also sweeps runtimes/.stage-* dirs nothing has written to within the stale rule. * feat(ssh): stream relay and runtime uploads over exec stdin when SFTP is refused (D5) On the bundled ssh2 transport to a POSIX host, a definite SFTP refusal (subsystem refused, sftp-server exited during the handshake, or a chrooted view answering NO_SUCH_FILE for a shell-created path) now falls back to writing through an exec channel's stdin, reusing makePosixWriteFileCommand with a byte-count check and atomic rename. Transport loss, timeouts and aborts never select the fallback. execCommand gains a stdin option. * test(ssh): answer the runtime store lock in the OpenCode runtime setup test Promotion now runs under runtimes/.store-lock, so the mocked host must grant the lock and the stage-exhaustion case makes four more round trips. * test(ssh): rung C relays never take the runtime store lock --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…ials (stablyai#24347) * fix(relay): let a draining cell pass restart-safe through refused redials A draining cell refuses every control and host proof, so once no session, splice, or queued byte remains, an in-flight or reserved connection unit can only belong to a dial the cell is about to refuse. The restart-safe wait no longer resets its pace-window streak on those units, and its progress line now prints every counter the gate reads. Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010 * test(relay): pin fail-closed parsing of handshake counters on a draining cell Claude-Session: ced32ebb-7155-4413-adad-1eccd14c2010
A browser tab has no native window buttons, but a Mac user agent still reserved that left gap. Desktop macOS keeps the inset. Co-authored-by: Cursor <cursoragent@cursor.com>
The inset token and the pad were each passed isWebClient by their own call site, so a test of the helpers could stay green while one site stopped reading the flag. Co-authored-by: Cursor <cursoragent@cursor.com>
innocarpe
force-pushed
the
fix-19299-web-traffic-light-pad
branch
from
October 1, 2026 10:05
dde585c to
d34dabb
Compare
7 of 9 tasks
Owner
Author
Sync update (
|
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.
Upstream
Summary
ELI5 Opening Orca in a browser on a Mac left an empty gap at the top left, where the desktop app's red, yellow, and green window buttons would be. The browser has no such buttons, so that gap is gone. The desktop Mac app still leaves room for them. ## What Changed Before, a
Note
innocarpe/orcamainuntil the upstream PR is merged.