Commit Graph
13672 Commits
Author SHA1 Message Date
OrcaWinandm4air fee083fa47 feat(runtime): session-only browser keys and opt-in for pairing from plain-http pages (host) (#27540)
* feat(runtime): session-only browser keys and an opt-in for pairing from plain-http pages (host)

- pairing.registerDeviceKey accepts ephemeral: true (advertised as
  pairing.device-key-ephemeral.v1). Such keyed rows are dropped when the host
  restarts and after 12 hours with no live socket; a sweep removes them and cuts
  any socket still bound to them, so later key auth gets device_revoked.
- The WebSocket upgrade Origin is recorded per socket. A first pairing (a
  never-used pairing code) from an http:// page on another machine is refused
  with a sealed insecure_origin error unless the operator sets
  ORCA_ALLOW_INSECURE_WEB_PAIRING=1. Already-paired browsers, https and loopback
  pages, and native clients are unchanged.
- Registry file reading moved to device-registry-file-read.ts to keep the
  registry under the size cap.

* fix(runtime): gate first pairings only for the marked web client, never phones

React Native sends an http Origin on every LAN WebSocket, so the Origin alone
refused first-time QR pairing from released phones. The web bundle now marks
its upgrade with ?orca-client=web; only a marked socket from an http page on
another machine is gated. Released hosts ignore the query string.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 14:40:36 -07:00
Brennan BensonandKelvin Amoaba 79a597375b test(native-chat): keep #26586's path-link cases as regression tests (#26819)
Command-, route-, version- and host-shaped text stays plain unless the
host confirms the path, in chat and terminal alike, since #26675.

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-10-11 14:38:56 -07:00
Brennan Benson fc1686efd6 fix: preserve module URLs in the headless server bundle (#26450) 2026-10-11 14:34:38 -07:00
Jinwoo Hong 8e01e6d9f3 test(e2e): layout oracle reads the live runtime layout (#27560)
* test(e2e): read the live runtime layout in the layout oracle

The oracle now reads the runtime's layout from a layout.subscribe consumer
(the window's renderer, or a headless host's pairing offer) instead of
session:get or the saved profile. Model rules run on every published frame,
replacing the 300 ms settle read and the 100 ms rule sampler; disk-format
rules and the round trip run on the saved profile at each settled step.
Views are compared to the live layout exactly for every group and tab kind.
The known-on-main list is refilled from runs of this oracle on main.

* test(e2e): known-on-main differences do not hold the oracle's settle wait

A difference main is known to keep never converges, so a step settles once
only known-on-main differences remain. The orcad title-after-restart entry
is narrowed to the scenario a run on main reproduced.

* test(e2e): oracle settle counts frames across sleeps, pairs renamed groups, flushes saves

The settle wait carries the frame count across reads, so a frame during the
sleep restarts the quiet window. The client check pairs a group listed under
another id with the runtime group holding the same tabs in the same order,
so only that rename is known on main and a dropped group still fails. The
window flushes main's debounced save before the saved-profile read.

* test(e2e): refill known-on-main from Linux CI and post-flush runs on main

Removes the two-writers saved-profile entry (gone once the window flushes
main's save). Re-adds client-reorder tab order and the cold-relaunch client
list, and adds the cold- and live-daemon relaunch tab-bar entries and the
Electron rename-after-restart entry, each reproduced by Linux CI run
38170663248 or a local run on main's code.

* test(e2e): scope known-on-main entries to the OS that reproduced them

Entries reproduced on one OS only carry a platform and match only there.
The cold-relaunch restart entry now matches only a single group whose tabs
and order are unchanged, so a reorder still fails.
2026-10-11 17:29:35 -04:00
Jinwoo Hong 50b3d2e34c fix(mobile): accept a fresh terminal snapshot after a stream ends (#27539)
* fix(mobile): accept a fresh terminal snapshot after a stream ends

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): drop the native chat resume's duplicate snapshot-mark clear

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): drop the native chat stream's snapshot-mark wiring

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): drop the refit adapter's dead snapshot-mark argument

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 17:23:20 -04:00
Brennan Benson 05458416af Revert "+" new-tab agent launches to the window route (reverts #26489) (#27173)
#26489 started no-prompt "+" launches through the host lane that is built for
terminals no window shows, into a pane the window already owns. That lane
differs from the window lane in start size, env, telemetry, failure handling
and Windows runtime resolution. Restore main's window route until the two
terminal lanes are converged, then move "+" onto the single lane.

This reverts commit 7634bbfffe.
2026-10-11 14:15:02 -07:00
Brennan Benson c07b045934 Replace three copied new-terminal tab-order blocks with the shared helper (#26771)
* Keep browser tabs in place when the + button opens a terminal

The + new-terminal path kept its own copy of the tab-order placement and
left browser tabs out, so in a workspace with no saved order a browser tab
could land after the new terminal. Use the shared persistAgentLaunchTabOrder
helper instead.

* Use the shared tab-order helper for the remaining new-terminal paths

handleNewTab's shell-override fallback and the menu new-terminal IPC
fallback each kept an inline copy of the "put the new tab last" order
logic. Both now call persistAgentLaunchTabOrder. The one behaviour
difference: the shared reconcile drops ids repeated in a saved order,
where the copies kept them.

* Drop unchecked casts from the new tab-order tests
2026-10-11 14:14:01 -07:00
Brennan Benson e0090f9bda fix(worktrees): keep the 'worktree is ready' toast until dismissed (#27566) 2026-10-11 14:08:16 -07:00
OrcaWinandm4air 34e2e5f469 refactor(remote): clients connect through a paired-connection handle, not a pairing offer (#27533)
Desktop, CLI, web and runtime transports now take a PairedRuntimeConnection
(endpoint, pinned host key, credential) instead of a PairingOffer. Offers stay on
the pairing input path (codes, QR, links, environment add) and are converted with
pairedRuntimeConnectionFromOffer. The credential is token-only today; request
envelopes still carry the same deviceToken, so the wire is unchanged.

This lets device-key and ticket credentials replace the token later without
touching every connect call site again.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 14:02:03 -07:00
Neil d65e9bed9f [CI] Improve test clocks and dependency fixtures (#27574) 2026-10-11 14:01:07 -07:00
OrcaWinandm4air 8e536f9514 refactor(terminal): paste reads the live transport (#27572)
Paste and drop planning no longer re-derive an SSH connection from the
worktree. After C1 no pane transport carries a relay connection, so the
'ssh' paste kind, the worktree fallback and terminal-paste-ssh-platform.ts
are gone.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 13:50:54 -07:00
Jinwoo Hong 92f6c867d1 chore(relay): move the staging cells onto image F (#27570)
* chore(relay): move the staging cells onto image F

Staging c1, c2 and c4-c7 run image F (main 3c1141f2), so the director and
every staging cell share one digest for the c5-c7 promotion and the
six-cell dead-cell rerun. c3 is unchanged.

* test(relay): pin the staging C4 digest to image F
2026-10-11 16:48:40 -04:00
OrcaWinandm4air f0b310b1b1 refactor(store): delete direct-SSH terminal ledgers (#27569)
Panes stopped spawning on unconverted ssh: rows in #27489, so the retry,
live-binding and history ledgers were written but never read. Delete them,
the retry and settle actions, and the detach transfer. The disconnect and
new-authority clears keep nulling stale ssh: PTY ids exactly as before.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 13:42:25 -07:00
OrcaWinandm4air 47c93636af refactor(terminal): drop the relay-held error token (#27564)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 13:26:53 -07:00
Neil 127ab13f1c [CI] Freeze legacy journal partition receipts to reduce test work (#27559)
* [CI] Freeze legacy journal partition receipts to reduce test work

* [CI] Type readonly journal fixture callbacks
2026-10-11 13:24:52 -07:00
Jinwoo Hong 3c1141f233 test(perf): layout latency instruments and a main baseline (#27538)
* test(perf): in-app layout latency probe, event-loop delay on main and orcad, and a main baseline

* test(perf): plain report comment

* test(perf): name the latency report type

* test(perf): add the Linux CI baseline; routine runs take 10 samples

* test(perf): stamp close pane on the w key, keep probe work out of samples, label first drawn and Linux flood
2026-10-11 16:13:17 -04:00
Brennan Benson bedf6f5a7b Session History: resume folder workspace chats in their own folder (#27209)
* fix(session-history): resume folder workspace sessions in their own folder

Session History matched a session to its workspace only among git worktrees, so a
conversation recorded in a folder workspace had no owner: its subagent rows could
never resume, and the parent row only offered whatever workspace happened to be
active. Folder workspaces now join the owner list as their usual folder: rows.

* test(session-history): mock folderWorkspaces as the array the store holds

* fix(session-history): jump through the shared workspace activator without a second toast

Both Session History jumps and the resume activation now go through
activateAndRevealWorkspace, which owns the folder routing and its path gate.
A folder the gate refuses already shows why, so the generic 'no longer
available' toast is kept only for a workspace that is actually gone.

* fix(session-history): keep the jump toast when folder activation could not pick a folder

Folder activation only explains a refusal once it resolves the folder's
owner; an id present on two hosts returns before any toast. Suppress the
generic toast only when that owner resolves.

* test(session-history): type the folder fixture without an assertion

* fix(session-history): call folder workspaces folders, not worktrees

Folder rows now read 'Resume in Folder', 'Folder' (details heading),
'Current folder' / 'Folder' / 'Archived folder', 'Jump to Folder', and
'Folder workspace is no longer available.'. The kind is derived from the
row's folder: workspace id; git worktree rows are unchanged.

* fix(session-history): say Jump to Folder in the row menus for folder rows

Also shows a plain folder icon on a folder row's details heading and aligns
the Spanish and Japanese folder strings with their worktree counterparts.
2026-10-11 13:09:18 -07:00
Jinwoo Hong 9ad45591af refactor(layout): keep compatibility code in named folders behind one entry (#27556)
* refactor(layout): keep compatibility code in named folders behind one entry

Moves the old-client translators into src/shared/workspace-layout/old-client/
and the remote layout snapshot recorder and persistence into
src/main/remote-layout-snapshot/, each behind an index.ts. A new
check-compat-boundary-imports script, run by pnpm lint and the PR workflow,
fails when code outside a compatibility folder imports past its index or
when a file other than the folder's named importer imports it. Moves only;
no behaviour change.

* test: drop whole-repo scan from boundary check unit tests; CI step runs it

* fix(lint): list only existing compatibility folders; resolve renderer aliases in renderer files
2026-10-11 16:06:05 -04:00
OrcaWinandm4air 8731ca9672 refactor(rpc): delete host-side terminal source-range acks (#27546)
The host side of the terminal source-range ACK protocol had one producer: the
relay-fed SSH output intake, deleted in #27515. Since then nothing installs the
runtime's source-range hooks and nothing passes source ranges into PTY output,
so every terminal.multiplex subscribe that offers ackOutputSourceRanges is
already answered without it and uses the byte-window ACK.

Delete the ledger, the per-connection registry, the consumer hook surface on
the runtime, the stream-level reservation/commit/rollback plumbing in flow
control, initial snapshot and cleanup, the source-range ACK frame schema, and
the range slicing in output chunks, batching and replay. The subscribe schema
still accepts the ackOutputSourceRanges capability, and the client keeps its
range-ACK support for older hosts. A new golden test pins that an offering
subscribe gets the same reply and frames as a byte-ACK one.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 13:04:34 -07:00
Brennan Benson 5b228ea71d One shared subagent tracker for Claude, Codex and Grok chat rows (#27228)
* test(native-chat): pin each agent's subagent row writes before unifying the rosters

* refactor(native-chat): one shared subagent tracker owns every agent's subagent rows

Claude, Codex and ACP (Grok) each kept their own subagent roster with drifted
rules. They now only report what they observe; one tracker decides grouping,
runs, names, state, sweeps, retention, row writes and restart inheritance.
The golden row-write tests from the previous commit show each rule change.

* fix(native-chat): one entry per subagent, derived lookups, ACP end stamped in its session-end write

Review round 1: a resumed child keeps its one entry and reopens it where it is
listed (the chat draws one section per child, anchored at the first row naming
it), every lookup is read from the rows, the no-turn row is never released,
and ACP's session end stamps its live children inside the session-end write
instead of writing rows ahead of it.

* fix(native-chat): derive the no-turn group, end a foreground run with the turn that started it

Review round 2: the row no turn owns is recognised from its id (a stored flag
was lost when an earlier run's row was first reached through one of its
children), and a turn's end settles the foreground runs it started wherever
their child is listed.

* fix(native-chat): count an inherited subagent row as settled history when a lookup loads it

A read-only lookup could load an earlier run's settled row that retention never saw, so it was never
released and a fresh settled row was released in its place.
2026-10-11 13:04:04 -07:00
Jinwoo HongandNeil e22fd9c901 feat(macos): show terminal-host Full Disk Access, tag stable-copy daemons, and give Helpers folder strings (#27354)
* fix(macos): preserve terminal privacy identity across app updates

* test(macos): make launchd fixtures portable across host platforms

* fix(macos): probe protected contents and close handles on denial

* fix(i18n): register terminal permission status labels

* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint

A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.

* feat(telemetry): split daemon origin events by stable-copy launch method

daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.

* refactor(telemetry): report the stable copy as a spawner path class

The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.

* refactor(settings): gate terminal-host Full Disk Access on macOS once

The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.

* fix(macos): keep the fork fallback inside the startup PTY gate

The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.

The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).

The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.

Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.

* docs(macos): ship the daemon launch rules, not the investigation

The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.

* fix(macos): hand off to the fork as soon as the stable job exits

A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.

Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.

* fix(macos): keep daemon copies out of Spotlight and unregister retired ones

Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.

New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.

Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.

* docs(macos): record the LaunchServices trade-off and the rollback plan

The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.

A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.

* fix(macos): keep daemon copies registered so Local Network can resolve them

The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.

* fix(macos): hide daemon copies from Spotlight and tie the job to Orca

Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.

Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.

* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node

Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.

* ci(macos): cache and prefetch the pinned Node before packaging

Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.

* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app

The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.

* docs(macos): describe the terminal host helper, its floor and its identity

* fix(macos): bound the terminal host boot check by its deadline and always clean up

A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.

* ci(macos): cache the pinned Node on its own key with no restore-keys fallback

Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.

* fix(macos): reject a universal slice for the terminal host and pin its name in one test

packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.

* fix(macos): pass the launchd adoption lease its options object after the main merge

* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands

* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper

* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree

* style(macos): use a template literal in the terminal host resolution check

* fix(macos): derive the terminal host's packages from the daemon's own module graph

* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies

* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up

Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.

The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.

* feat(macos): report terminal-host Full Disk Access, classify stable-copy daemons, and give Helpers folder strings

Restores what a32018836b moved out of the terminal host change:
- daemon adoption telemetry classifies a daemon spawned from the private copy as `stable-copy`;
- Settings shows the terminal hosts' own Full Disk Access verdict beside the app's, through a
  new `fullDiskAccessStatus` daemon request that older daemons answer as unknown;
- packaging writes the Desktop, Documents and Downloads descriptions into the app's Helper
  bundles, which still host terminals when the fork fallback runs.

* fix(i18n): restore en.json to this branch's base, not newer main

The previous commit took the file from a newer main, which pulled unrelated strings into this PR.

* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label

Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.

* fix(packaging): copy terminal-host dependencies from where Node resolves them

A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.

* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice

An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.

* docs(macos): describe the per-build terminal-host copy and its fallback reasons

* docs(macos): keep the codesign command on one line

* test(macos): type the launchd test state without an assertion

* docs(macos): name what the login-session flag gates in the stable launcher

* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label

* refactor(daemon): keep the health request types in their own module

types.ts hit the line limit once the Full Disk Access status request joined the union.

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-11 16:03:26 -04:00
Brennan Benson 37c2b43876 fix(native-chat): put Continue in the card that says the reply stopped (#27097)
* fix(native-chat): put Continue in the card that says Orca stopped the reply

Continue sat in its own row above the message box, away from the
reply it continues and often outside the chat column. It now sits
inside the transcript's notice card that says why the reply stopped,
as a filled primary button beside that sentence.

* fix(native-chat): one Continue that the stop card never repeats

- A paired server with no name is called "the remote server", so the card
  words the stop from its cause instead of the host's stored "You can
  continue in this conversation." beside the Continue button.
- Continue is an xs primary button that fits the text row without a margin
  workaround.
- continueNow is a plain useCallback: its inputs (target, session id, the
  composer error reporter) are already stable.
- Test the real chain: a cut journal through the structured session shows
  exactly one Continue, inside the stop card.
2026-10-11 13:01:52 -07:00
Brennan Benson 28d07a6b63 fix(mobile): open Grok, OpenCode, OMP and Pi chats on the phone (#27212)
* fix(mobile): open Grok, OpenCode, OMP and Pi chats on the phone

The phone advertised only the Claude chat lane, so the host retitled every
other agent's chat tab 'Update to view' and the phone's resolver refused
non-Claude/Codex chat tabs anyway. Mobile now reads any agent the host
lists, passes the chat's real agent to its send and slash-command paths,
advertises the registered-agents and Pi dialog capabilities, hides image
attach for agents the host says take none (as desktop does), and draws
Pi's input dialog as one line.

* test(mobile): drive the agents read through the shared fake client

* fix(mobile): read options reports with no model

Grok, OpenCode and OMP can report no current model, and a chat at rest may
have none saved. The host refuses those reports to clients that don't
advertise agent-session.options.optional-model.v1, which left the phone's
model picker on its seed list and dropped /clear and /compact.

* fix(mobile): reserve the chat session for every launched agent

The phone matches a launched chat tab only by its session id, which it
minted only for Claude and Codex. A Grok, OpenCode, OMP or Pi chat started
from + landed only once the host replied, and a lost reply read as
"Couldn't confirm the agent started" even with the chat listed. Hosts
accept an agent-named id for any agent and ignore it for a terminal launch.

* fix(mobile): read the host's agents once per connection, retry after a failure

Every session-screen mount asked agentSession.agents again, so the attach
button popped in late for OpenCode and Pi, and a read that failed while
connected was never retried. Share one read per client, drop it on
disconnect, and forget a failed read so the next chat asks again.

* test(mobile): pin the chat's own agent on the send path and attach gating

The send-bridge test passed with the claude/codex narrowing reverted. Drive
an OpenCode chat through the controller and the structured session's send,
where /review is the agent's own command but Codex's host claims it, and
check the overlay offers attach only when the agent takes images.

* fix(mobile): offer a card's X only while a turn is running

The X on a structured prompt card stops its turn. Pi opens dialogs outside
a turn too, and tapping X there could only say "Stop not sent". Claude and
Codex cards arrive inside a running turn, so they keep their X.

* fix(runtime): let a phone read the host's registered agents

The phone now asks agentSession.agents to decide whether a chat's agent
takes images, but the mobile allowlist refused the method, so OpenCode and
Pi never got an attach button. It returns only each agent's id and its
fixed capability record.

* fix(mobile): keep a host's refusal of the agents list for the connection

Released hosts advertise registered agents but refuse agentSession.agents
to phones (forbidden, not available to mobile clients). Treat that answer
as no list until the next connection instead of re-asking on every chat;
keep retrying transient failures.

* fix(mobile): shrink the model pill before it pushes Send off the composer row

On a narrow phone, attach + a long model label + mic left no room for Send
(seen with Pi's resting label). The pill's slot is now the row's only
shrinkable item, so its label truncates and the buttons keep their size.
Rows that already fit look the same.

* fix(mobile): keep /clear and /compact for a chat whose agent lists no models

The phone dropped a structured chat's option controller when its picker had
nothing to show, and the / menu reads the chat's conversation commands from
that controller. OMP lists no models, so its menu lost /clear and /compact.
Keep the controller for every structured chat; the picker still renders
nothing without a model.

* feat(runtime): say in a chat's options report whether it takes images

The host's options read now carries acceptsImages for every session: the
agent definition's imagePrompts, narrowed by a running ACP child to what
its initialize advertised (the same rule its sends use). Absent means an
older host.

* refactor(mobile): read whether a chat takes images from its options report

The phone no longer reads the host's agent list for the attach button. It
takes acceptsImages from the agentSession.options read it already makes,
keyed to the chat, and kept through a pick made during that read. Before
the read answers, and from an older host, Claude and Codex keep attach and
other agents don't, as before. Drops the agent-list hook with its cache and
refusal latch, the host-support flag only it used, and agentSession.agents
from the host's mobile allowlist.

* refactor(native-chat): gate desktop chat images on the session's options report

The composer's image gate takes the session's acceptsImages answer first,
falling back to the host's agent list before the options read lands (a
new chat's start) and for an older host that sends no answer. With the
phone no longer using it, structuredAgentAcceptsImages moves back to the
renderer, where it was.

* test(native-chat): prove the session's image answer reaches the composer gate
2026-10-11 13:00:56 -07:00
Jinwoo Hong 7d14ac0bcb refactor(layout): add the editor tab view type to the layout model (unused) (#27541)
* refactor(layout): add the editor tab view type to the layout model (unused)

Move the open-file sub-types a diff, markdown preview, conflict, check-details or chat-visual tab
carries into src/shared, and give the layout model's editor record an optional view keyed by mode.
The Serializer leaves it off disk, so the saved session is byte-identical. Nothing sets it yet.

* refactor(layout): hold non-file editor views by tab id, not on the file record

Diff, preview, conflict-review, check-details and chat-visual tabs use a worktree root or a
synthetic id as their path, so on the file record they would collide and save as edit tabs. The
file record keeps only the edit-tab conflict view; the others live in an unsaved map keyed by tab
id, set by openEditorTab's new optional view and dropped when the tab closes.

* refactor(layout): keep every editor tab view in the one unsaved map

Edit-tab conflict details join the other views in the per-workspace map keyed by tab id, so the
file record and the Serializer are unchanged and there is one place to read a view.
2026-10-11 15:56:49 -04:00
OrcaWinandm4air b15c2c6dd6 refactor(persistence): retire the dead SSH PTY lease writers (#27555)
* test(persistence): seed relay-era SSH lease rows from disk

* refactor(persistence): retire the dead SSH PTY lease writers

* test(persistence): read relay-era recovery rows without a cast

* chore(ratchet): prune layout-writer baseline after lease writer removal

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:46:50 -07:00
Jinwoo Hong 1c4f16e212 chore(ratchet): prune main layout-writer baseline after #27529 (#27554)
#27431 and #27529 landed together; #27529 removed a writer from each spawn-commit file, so the ratchet now fails on main.
2026-10-11 15:45:36 -04:00
Jinwoo Hong a0b0328f99 fix(ci): a removed layout writer no longer fails other PRs' ratchet check (#27557)
#27529 removed two main-process layout writers after the ratchet landed, so main's
baseline went stale and every PR failed the check. Prune the baseline and report a
shrunk count as a warning; only growth fails.
2026-10-11 15:38:44 -04:00
Jinwoo Hong 738e0e034b refactor(layout): add the per-view store, pane status and runtime leaf-record projection (unused) (#27535)
* refactor(layout): add the per-view store, pane status and runtime leaf-record projection (unused)

Pure pieces the layout holder will use, with no caller yet:
- per-view records keyed by view id (desktop, device:<id>), read-time
  fallback for removed tabs and panes, save-time prune and workspace removal;
- pane status derived from the start token, last outcome, sleeping record,
  PTY registry and host reachability;
- the runtime's tab and leaf records projected from the layout model and the
  PTY registry in the graph mirror's shape, checked against records captured
  from today's mirror.

* fix(layout): follow today's close selection, title resets and lost-host status in P6

- A close moves each view's shown tab when applied, with the pre-close
  order: most recent remaining, else the closed tab's neighbour, through
  the headless close's pickNextActiveTabAfterClose (now exported).
- The projection's pane title is the newer of the PTY's OSC title and the
  neutral reset the window applies after an agent exits or is interrupted;
  those reset rules move to shared/terminal-pane-title-reset.ts and the
  renderer uses them unchanged.
- A leaf is connected, as in the mirror, unless the host proved an exit,
  but only when the PTY registry holds the terminal.
- An unbound pane on a lost host is waiting_for_host.
- The mirror capture covers a title reset, a lost host and an unregistered
  PTY, and names each difference from the mirror.

* fix(layout): an emptied group hands a view's active group to its split sibling, as collapseGroupLayout does
2026-10-11 15:35:25 -04:00
Jinwoo Hong dbd97f7d67 refactor(terminal): keep mobile content-fact caches in their producers (#27537)
The agent-status projection and mobile terminal theme kept their caches on
the shared graph state object. Each producer now owns its cache, so neither
imports the graph state or graph types; the graph still calls them as before.
2026-10-11 15:20:24 -04:00
Jinwoo HongandClaude 5e5865a226 refactor(layout): drop unused snapshot receipt fields and add the snapshot read method (#27536)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-11 15:20:21 -04:00
OrcaWinandm4air f9794d8efc refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients (#27523)
* refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients

Every paired client now builds its hello, checks the host's ready, seals its
auth frame and opens host frames through src/shared/paired-e2ee-client-handshake.
Platforms inject their own NaCl/base64 primitives. The v2 client session and its
pure-JS key schedule (@noble/hashes 1.8.0, the version mobile pins) move to
src/shared, and the per-host v2 latch policy is shared with an injected store.

No wire change: frames match the captured legacy and v2 fixtures byte for byte,
and a cross-version test drives the core against the released host and the
released client against this host. The auth step is now async so a device-key
signer can slot in later.

* fix(remote): keep @noble/hashes out of the CLI's unbundled runtime

The packaged CLI loads src/shared output from disk, so a bare @noble/hashes
import failed the packaged-runtime check. The v2 key schedule is now a
primitive: Node clients use the node:crypto schedule (moved to src/shared so
the CLI can reach it), while the browser and phone keep the pure-JS one.

* fix(mobile): re-check the v2 latch before sending a legacy auth frame

The auth step is async, so another socket can latch the host to v2 while a
legacy auth frame is being sealed. Refuse it at send time, as before.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:14:51 -07:00
Jinwoo Hong 1674269686 feat(workspace-layout): pure old-client translators for layout methods (inert) (#27534)
* feat(workspace-layout): add pure old-client translators for layout methods

Unused by main. Translates old session.tabs.* and terminal.* params into
layout commands (updatePaneLayout per rule 11, terminal.create {tabId,
leafId} as start/split/create) and projects the model into today's
session.tabs snapshot rows, with customTitle read first (#19582).

* fix(workspace-layout): match today's old-client behaviour in the translators

CLI terminal.close/closeTab keep the pinned refusal; terminal titles use
resolveTerminalTabTitle with the generated-titles setting and the sole-pane
rule; session.tabs.move keeps today's exact-order validation and error
codes; setTabProps on an unknown tab is a no-op; drop the close force
input the wire does not carry.

* fix(workspace-layout): phone tab close keeps today's pinned refusal

A terminal tab row or a tab's last pane refuses a pinned tab
(terminal_tab_pinned) with or without a window, as today; a pane of a
split tab and non-terminal tabs close as asked.
2026-10-11 15:11:09 -04:00
OrcaWinandm4air 2779dfeb2b refactor(pty): drop SSH spawn, liveness and kill branches (#27529)
* refactor(pty): drop SSH spawn, liveness and kill branches

* test(pty): drive the commit-durability tests through local PTYs only

* test(pty): drop the SSH host arm from the exit-durability fixture

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 12:05:22 -07:00
Jinwoo Hong e969881be5 chore(ratchet): layout-writer ratchets for main and renderer (#27431)
* chore(ratchet): layout-writer ratchets for main and renderer

Two per-file ratchets that may only go down: main-process callers of the
Store's layout setters plus direct edits of saved session partitions, and
renderer writes of the window's layout and per-view selection fields
(including computed-key and spread writes, counted where the key or object
is built). The frozen-mirror writers are exempt. Wired next to the
focus-read ratchet in lint and the PR workflow.

* chore(ratchet): count only membership writes of editor and browser entries

A .map over the existing openFiles or browser tab list keeps its entries
and order, so it is a content update (draft, git status, URL) and is no
longer counted by the renderer layout-writer ratchet.

* chore(ratchet): ignore variable keys and pass-through shorthands

A variable key such as exitCodes[ptyId] names no field, so only string
keys count. A shorthand key counts only in an object handed to set,
setState or a Store writer; elsewhere it passes a value on.

* chore(ratchet): prune renderer baseline after main moved
2026-10-11 14:55:11 -04:00
Jinwoo HongandNeil 3f8d296b8d fix(macos): preserve terminal permissions across app updates with a small signed helper (#27021)
* fix(macos): preserve terminal privacy identity across app updates

* test(macos): make launchd fixtures portable across host platforms

* fix(macos): probe protected contents and close handles on denial

* fix(i18n): register terminal permission status labels

* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint

A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.

* feat(telemetry): split daemon origin events by stable-copy launch method

daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.

* refactor(telemetry): report the stable copy as a spawner path class

The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.

* refactor(settings): gate terminal-host Full Disk Access on macOS once

The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.

* fix(macos): keep the fork fallback inside the startup PTY gate

The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.

The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).

The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.

Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.

* docs(macos): ship the daemon launch rules, not the investigation

The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.

* fix(macos): hand off to the fork as soon as the stable job exits

A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.

Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.

* fix(macos): keep daemon copies out of Spotlight and unregister retired ones

Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.

New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.

Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.

* docs(macos): record the LaunchServices trade-off and the rollback plan

The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.

A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.

* fix(macos): keep daemon copies registered so Local Network can resolve them

The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.

* fix(macos): hide daemon copies from Spotlight and tie the job to Orca

Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.

Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.

* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node

Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.

* ci(macos): cache and prefetch the pinned Node before packaging

Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.

* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app

The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.

* docs(macos): describe the terminal host helper, its floor and its identity

* fix(macos): bound the terminal host boot check by its deadline and always clean up

A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.

* ci(macos): cache the pinned Node on its own key with no restore-keys fallback

Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.

* fix(macos): reject a universal slice for the terminal host and pin its name in one test

packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.

* fix(macos): pass the launchd adoption lease its options object after the main merge

* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands

* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper

* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree

* style(macos): use a template literal in the terminal host resolution check

* fix(macos): derive the terminal host's packages from the daemon's own module graph

* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies

* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up

Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.

The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.

* fix(i18n): restore en.json to this branch's base, not newer main

The previous commit took the file from a newer main, which pulled unrelated strings into this PR.

* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label

Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.

* fix(packaging): copy terminal-host dependencies from where Node resolves them

A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.

* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice

An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.

* docs(macos): describe the per-build terminal-host copy and its fallback reasons

* docs(macos): keep the codesign command on one line

* test(macos): type the launchd test state without an assertion

* docs(macos): name what the login-session flag gates in the stable launcher

* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-11 14:54:36 -04:00
Neil e7da462dbf [CI] Run focused checks for Mobile CI tool changes (#27532) 2026-10-11 11:49:05 -07:00
OrcaWinandm4air 3359fd2bfc refactor(ipc): worktree handlers are local or runtime only (#27528)
* refactor(ipc): worktree handlers are local or runtime only

* refactor(runtime): explain the route witness on removal listing

* test(runtime): pin the SSH create refusal and drop the deleted branch's reliability gap

* test(runtime): drop a needless cast in the SSH create refusal test

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 11:48:57 -07:00
Jinwoo Hong 448b9f6fe9 refactor(layout): add the pane start, exit, first-surface and rename rules (unused) (#27432)
* refactor(layout): add the pane start, exit, first-surface and rename rules

Pure rules in the shared layout model, with no caller yet:
- startPane leaves a bound pane, or one whose start is in flight, alone;
- a process exit retires its pane on code 0, keeps today's keep cases as
  exited, and only unbinds on an unproven exit or attach absence;
- a per-pane last outcome, cleared by startPane and restartPane;
- createTerminalTab/openAgentSessionTab `initial: restore | activation`;
- editor tabs and their file records follow a renamed file.

* fix(layout): keep a newborn pane's exit whatever its code, as today

* fix(layout): keep a split by today's facts and report renamed tab ids
2026-10-11 14:38:56 -04:00
Jinwoo Hong 73c53311dc refactor(terminal): move the mounted-terminal registry out of the graph file (#27427)
registerRuntimeTerminalTab, hasRegisteredRuntimeTerminalTab,
focusRuntimeTerminalSurface and the mounted-tab maps now live in
runtime/mounted-terminal-tab-registry.ts. The runtime graph subscribes to
registry changes and still schedules a graph sync on mount, unmount and an
explicit leaf focus, exactly as before. No behaviour change.
2026-10-11 14:26:22 -04:00
Jinwoo Hong 7ce15106f3 fix(relay): keep verifying host tokens from the last served keys while auth is down (#27490)
* fix(relay): keep verifying host tokens from the last served keys while auth is down

jose refetches the relay JWKS once its cache is 10 min old and rejects the token when that
fetch fails, so an auth outage turned every still-valid host token into a 4401. The cell now
falls back to the last set auth served for up to an hour, skips refetching for 30 s after a
failure, and still rejects any key that set never had. A fresh set replaces it as soon as auth
answers, so a rotation takes effect at the first successful fetch.

* fix(relay): bound stale relay keys at 30 min and document the emergency purge

Thirty minutes still covers a full auth outage (no token the stale keys could accept outlives
~16 min of one) and halves how long a rotated-out key stays trusted on a cell kept from auth.
A restarted process holds no set and refuses host tokens until a fetch succeeds; the ops doc
names that as the purge after a signing-key leak, and a test pins the fail-closed start.

* test(relay): pin the 30 min stale relay key bound (29 min accepts, 31 min rejects)
2026-10-11 14:25:29 -04:00
Jinwoo Hong 4af35473eb fix(relay): a resumed host control adopts the token its hello proved (#27487)
* fix(relay): a resumed host control adopts the token its hello proved

A rebind replaced the socket, lease and epoch but kept the predecessor's token claims, so a
host that resumed after its token lapsed (auth down past expiry plus grace) was closed 4401
"relay authorization expired" at the next heartbeat, 15 s later, and every resume repeated it.
The rebind now takes the hello's verified claims when they carry a later expiry, and a hello
that speaks for another profile or organization starts a new generation instead of resuming.

* fix(relay): a refresh never lowers the held expiry and closes only its own socket

An auth refresh now keeps the later of the held and refreshed expiry, the rule a resume uses.
A refused refresh, or one that meets an idle cutover, closes the socket it arrived on: after a
rebind, session.socket is the successor, which proved itself and must not be closed for its
predecessor's late frame. Adds an organization-mismatch resume test beside the profile one.

* test(relay): a superseded socket's refresh during an idle cutover closes only that socket
2026-10-11 14:25:11 -04:00
Jinwoo Hong 735e6d6c5d fix(relay): let a host leave a dead reserve cell without an operator fence (#27458)
* fix(relay): let a host leave a dead reserve cell without an operator fence

A host whose reserve home cell died got 503 relay_home_cell_unavailable until the
cell came back: the database path demands a committed fence for every capped cell.
On its own /v1/assign, a host now moves off a reserve cell (Postgres records it as
reserve) when the cell's heartbeat is stale and this director's seat-feed polls have
gone unanswered for 45 s, unless more than a third of the polled cells look the same.
Sweeps, migration pins and capped db cells keep the fence.

* fix(relay): guard the dead-cell verdict by region and sample size, and floor the move on the map

- No verdict when half or more of the dead cell's own region fails with it (a cross-region
  partition), more than a third of the fleet does, or fewer than 6 cells are polled. A withheld
  region or fleet verdict logs orca_relay_dead_cell_verdict_withheld, which alerts.
- The move needs the step-5 plan's epoch floor, so it mints above every seat the map knows;
  without the plan the fence is kept. A test revives the old cell's seat above the row and checks
  that the ledger never writes the row back.
- The move's log line carries the fleet and region counts.

* fix(relay): count only feed cells and young streaks in the dead-cell guards; gate the alert

- Denominators hold only cells that served this director a feed page and have capacity: legacy
  rows answering 404 no longer fill the 6-cell sample or dilute the fleet and region ratios.
- A peer counts as failing for the guards on any current streak (a failure in the last 5 s), so a
  staggered partition cannot let its first cell cross 45 s while its neighbours read healthy.
- Every withheld verdict is logged, sample included (a cell missing from the feed cells is one).
- Tests: ready=0 and enabled=0 with failing polls keep the fence (drain then death is fenced).
- The withheld metric and alert are gated by relay_dead_cell_verdict_alert (default false, on in
  staging); the alert docs now describe poll failures, not heartbeats.

* fix(relay): always sample the dead-cell candidate, so a restarted director can still judge it

A director that restarted after a reserve cell died never received a feed page from it, so the
cell was never in the sample and its verdict stayed withheld as 'sample'. The candidate now always
counts when it has capacity (the store asks only about a cell Postgres records as reserve, and only
feed images run reserve); peers still count only once they have served a feed. Tests: a restart
during a lone outage decides, a partition begun before the restart withholds by region, and the
capacity filter. The Terraform comments now say to apply the metric first and the policy minutes later.
2026-10-11 14:24:52 -04:00
Jinwoo Hong 1bf36692e9 fix(relay): let the staging admission workflow register and promote c5-c7 (#27491)
The staging admission config hard-wired staging-gce-c4, so new staging cells
had no reviewed path into the admission selector. Add c5-c7 as a second
staging wave at their US shape; production cell shapes are unchanged.
2026-10-11 14:24:34 -04:00
Jinwoo Hong ea61d1c03e docs(relay): director Cloud SQL sidecar enablement and rollback (#27461)
* docs(relay): director Cloud SQL sidecar enablement and rollback, as run on staging

The 0% apply revision is not started by Cloud Run, so the first sidecar boot is the deploy's
rollback candidate. A new log-based metric can take minutes to appear, so its alert policy
can fail the first apply. Both observed on staging 2026-10-11.

* docs(relay): production sidecar enablement needs the tfvars PR, a passed drill and the rollout lease
2026-10-11 14:24:15 -04:00
Jinwoo Hong e9dc9534d7 fix(relay): cell-flags tool refuses reserve without readinessLocal=true (#27460)
A reserve cell's dead-man is fed only by director polls through the load balancer. With SQL
readiness the balancer drops the cell during a database outage, the polls get 503
failed_to_pick_backend, and the cell trips (staging drill 2026-10-11, c2 at 11:47:59Z).

admitMode=reserve now needs readinessLocal=true in the object being written, already there
or set in the same write, and a reserve cell refuses readinessLocal=false unless the same
write flips it back to db.
2026-10-11 14:23:56 -04:00
Neil 48d76638a3 [CI] Speed up Mobile test execution (#27521)
* [CI] Speed up Mobile test execution

* [CI] Check rebuild watch registrations directly
2026-10-11 11:22:31 -07:00
Jinjing 45de8d0791 Theme Mermaid dark diagrams with the app's chart colors (#27398)
The legacy Mermaid dark theme drops color from flowcharts and uses light Gantt surfaces. Dark previews now use the `base` theme with palette values read from the app's CSS tokens. Chart tokens can resolve to oklch(), which Mermaid does not accept, so colors are converted to hex through a canvas. Tests cover Gantt label contrast and distinct pie colors.
2026-10-11 11:16:41 -07:00
Brennan Benson ac529511b9 A closed chat tab stays closed instead of coming back on the right (#27205)
* fix(native-chat): a closed chat tab stays closed while the host is still closing it

Closing a chat tab removes it from the window at once. A tab-list frame the
host publishes before it has handled the close (a working chat's status
update, for one) still listed the chat, and the window appended it back at
the end of the strip until the host's own removal arrived.

The window now leaves out a chat whose close it has not heard back on, using
the in-flight close record it already keeps, and re-reads the host's tab list
once the close settles so a chat the host kept comes back.

* fix(native-chat): hide a closing chat tab through the existing close intent

Record a close intent for agent-session:<id> under the chat's host when a chat
tab close begins, so a host frame sent before the host handles the close cannot
re-add the chat at the end of the strip. The intent lifts when a host frame
stops listing the chat, or by its TTL. Only a failed host close clears it and
re-reads that worktree at the version already applied, so the chat the host
kept comes back.

Replaces the close-owner gate and the re-read after every close.

* fix(native-chat): end a chat close intent even when no host frame follows

Re-read the closed chat's worktree after every settled close, not only after a
failure, so a close the host had nothing new to publish for still ends its
intent; otherwise the same chat reopened within the intent's 20 s limit stayed
hidden. Skip the intent for floating-panel chats, whose frames are never
applied. Share one one-worktree local refresh with the Session History reopen
path. Reset close intents between tests in the reopened-tab e2e file, whose
harness tears the host down before a close's follow-up re-read lands.

* fix(native-chat): re-read after a successful chat close only for paired servers

This machine's host emits its removal frame before the close answers, and the
window applies it, so a local chat re-reads only when the close fails. A paired
server's frames travel apart from its answer, so, as for its terminal closes,
every close is followed by a read, and that read no longer joins a tab-list
request that began before the close. Floating-panel chats record no intent and
get no read.

* fix(native-chat): pin a chat close intent to the pairing the close began under

Record and clear the intent with one owner captured at close start, as paired
terminal closes do, so re-pairing a server mid-close cannot miss the clear, and
pass that pairing to the paired re-read.
2026-10-11 11:13:51 -07:00
Brennan Benson 7dfa4d6e54 fix(native-chat): a new chat's model and effort pickers match their first frame before the agent starts (#27031)
* fix(native-chat): a new chat's effort pill keeps its first-frame value before the agent starts

A chat with no model pick runs the catalog's default model at that model's
default effort. The at-rest options answer now says so for every agent, not
only those whose live child reports the model's default, so the effort pill
no longer blanks and then jumps when the agent starts.

* fix(native-chat): read a chat's at-rest options for the folder it runs in

The at-rest options read asked the model catalog with no workspace, so it
always named the account's default model and effort, even in a repo whose
own agent config could pick another. The new chat's first frame, read for
that repo, names nothing there. The at-rest read now asks for the session's
own folder, so both frames agree on the model and the effort.

* fix(native-chat): scope the at-rest options read only for clients that take no model

A client that needs a model in every options answer (mobile today, older
desktops) refuses an answer naming none, and loses that read's commands with
it. Only a client advertising the optional-model capability gets the answer
for the chat's own folder; any other keeps today's account-default answer.

* test: run the at-rest workspace-scope RPC test in the Node SQLite project

* test: build catalog listings so the fixtures compile before and after main drops the tier map

* fix(native-chat): one at-rest rule — the chat's folder decides its default for every client

The folder-scoped catalog read now decides the default for every client. Where
it names one, the answer names that model and its effort. Where it names none,
a client that takes a model-less answer gets none, and one that needs a model
gets the account default model with no effort: exactly what it got before.

The reader is now required, and the first frame and the at-rest read share one
rule for which locations name a local folder.

* fix(native-chat): keep test callers of the host's options read untouched

Only in-process callers omit the reader at the host's entry, and they read
as the RPC gate treats an in-process caller. The shared read itself still
requires one. This leaves main's newer test call sites unconflicted.

* refactor: one native-local location check for Claude, Codex and the at-rest folder read
2026-10-11 10:46:48 -07:00
OrcaWinandm4air 0df25b7ac9 refactor(pty): delete relay-fed SSH output intake and projection threading (#27515)
Nothing in production feeds the SSH PTY output intake: its only data and exit
entry points were called from tests, and the SSH PTY provider that would have
fed it is gone. Delete the intake, its source-obligation ledger, model
admission, projection publication and source-ack plumbing, and strip the
projection ids and source-credit bookkeeping they threaded through the local
renderer delivery path (accept, pending, flush, payload, accounting, exit,
session). Local terminal delivery is unchanged.

Because the intake also installed the runtime's remote source-range hooks,
this desktop host now answers a terminal.subscribe that asks for
ackOutputSourceRanges the way every orcad host already does: without the
capability, so the client uses the byte-window ACK. No source ranges were
ever attached to local output.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-11 10:25:42 -07:00