* test(daemon): run the stalled-reader backpressure test over a Unix socket, not TCP loopback
Linux autotunes TCP loopback buffers to several MiB, so the kernel absorbed
most of the 8 MiB flood, write callbacks completed, and the producer never
reached the 4 MiB high-water mark on some CI runners. The daemon stream runs
over a Unix socket or named pipe, whose small fixed kernel buffers keep the
backlog in the daemon's own accounting.
* test(daemon): bound Unix socket fixture paths
* feat(layout-owner): a CLI or phone file on a layout-owner server opens in the group a paired desktop shows
Each paired desktop reports, through a new layout.reportView (served only by layout.intents.v1
servers, never to phones), the group it shows in each workspace the server's frames draw. The
server keeps reports per connection beside the window's, opens files in the latest one's group,
and drops a connection's reports when it closes, falling back to the saved view.
* test(e2e): a CLI file open on a switch-on server lands in the group a paired desktop shows
* fix(layout-owner): layout.reportView refuses a caller with no connection, as sidebarInputs.reportView does
* feat(layout-owner): a desktop's re-sent view after a reconnect never outranks another desktop's live report
layout.reportView takes an optional refresh flag. The desktop's first report of each workspace
after a stream snapshot carries it; the server ranks such entries below every live report, while
they still replace the connection's own earlier entries and beat the saved view.
* feat(layout-owner): a paired desktop re-sends its view, as a refresh, when its control connection to the server comes up on a new socket
The server drops a connection's view reports when it closes, and the layout stream can stay up
across that, so the desktop watches the shared control diagnostics for a new socket.
* fix(layout-owner): a desktop's refresh keeps its own live rank; stop reporting to a server without layout.reportView
A same-connection resubscribe re-sends its view as a refresh, which demoted the connection's
own live report below an older desktop's. Ranks are now an explicit live/refresh tier. A pre-PR
switch-on server answers method_not_found; the desktop then stops sending for that stream.
* fix(layout-owner): a group the server's own frames move is re-sent as a refresh, never a live report
The snapshot resend read the store before the sync applied the snapshot, so the flush's group
change reached the store subscription as the user's. Writes made by the frame sync now report as
refreshes, and the snapshot resend runs after the flush.
* test(layout-owner): a change frame's group move goes out as a refresh; the frame-write mark survives nested writes
GitLab task items now carry the host of the repo row that fetched them, and the
project-view dialog stamps the row its slug matched. Both resolve through one
provider-neutral helper (getWorkItemRepo, generalized from #31252's GitHub-only
getGitHubWorkItemRepo), so a repo id published by two hosts never routes an
action to the other host's row.
* feat(agent.launch): a launch can name chat or terminal, and the CLI gets --surface chat|terminal
agent.launch gains an optional `surface` field, read by hosts advertising
agent.launch.surface.v1. `terminal` always runs the agent's CLI; `chat` runs a
chat even when the user's default is a terminal, and one the host can prove it
cannot open is refused with a typed no-effects error before anything is
created, so a retry answers with the same refusal. The pick is part of the
replay fingerprint. `orca agent launch` and `orca worktree create --agent`
accept --surface chat|terminal and refuse it on a host that would drop it,
including the legacy worktree.create fallback.
* test(agent.launch): pin the surface pick beside the other pre-start failures
* fix(agent.launch): settle requested surfaces from resolved workspace facts
* fix(agent.launch): describe fallback boundaries and correct test types
* feat(ai-vault): name the host each search hit came from when hits span several hosts (#31139)
An All hosts full-text search returned one flat list, and the same title or
path often came up on two machines, so you could resume the wrong copy. When
hits come from two or more hosts, each row now carries the same host chip the
sidebar uses, labelled the way the host menu names that host. Hit order is
unchanged; a single-host result shows no chip.
Reported, with the proposed approach, by @quzard in #31139.
* fix(ai-vault): let the search-hit host chip shrink in a narrow sidebar
* fix(ai-vault): keep sessions recorded under a symlinked workspace's real path in Workspace scope (#31139)
A workspace registered through a symlink (~/work/api -> /data/src/api, or
macOS /var -> /private/var) lost every session whose agent recorded the
resolved cwd: scope matching compared strings, and those rows also counted
against the out-of-scope depth cap.
The host that owns the files (the desktop for local scans, orcad for SSH and
server hosts) now resolves each scope path's real path, scans both spellings,
and gives matching sessions optional matching-only cwdAliases in the
registered spelling. Workspace scope, the depth cap and project attribution
read cwd plus aliases; resume still uses the recorded cwd. Older hosts send
no aliases and older clients strip the field.
Reported, with the proposed approach, by @quzard in #31139.
* fix(ai-vault): rank aliased attribution by closest root; never resolve recorded cwds
Review feedback: a longer-spelled parent link could take a nested project's
session, and resolving every out-of-scope cwd could stall a listing on a hung
network mount or raise macOS privacy prompts.
* perf(worktrees): gate the 30s git-common reconciliation sweep on each entry's dir signature
The native-watch reconciliation forced an ungated stat of every leaf of every
worktree admin entry each tick (~7 stats/entry). It now uses the entry-dir
signature gate plus the two leaves git writes in place (gitdir, logs/HEAD),
runs the ungated sweep every 10 ticks (~5 min), and falls back to sweeping
every tick if a sweep ever finds a change the dir signature missed.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(worktrees): only distrust the reconciliation dir gate on a confirmed, repeated miss
Detect a missed change on any entry rescan, including ones the in-place leaf
probes trigger, not just full sweeps. Re-stat the admin dir after the leaf
rescan so a rename racing the scan isn't counted, ignore transient missing
leaves, and require misses on two separate ticks before sweeping every tick.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(editor): drop isWholeEditorState and the diff-signature re-export; bound superseded restore re-reads
- Remove isWholeEditorState; its callers take the real AppState. Tests that
mocked partial stores now build full states from createTestStore.
- Import getDiskBaselineSignature from shared/disk-baseline-signature
directly and delete the re-export from diff-content-signature.ts.
- Restored-tab conflict scan: count superseded re-reads toward
MAX_VERIFY_ATTEMPTS, so a read slower than the re-verify interval gives up
with the conflict banner instead of looping forever. Adds a regression test.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): keep the last read at the superseded re-read cap instead of giving up
A matching read at the cap cleared nothing and showed a false changed-on-disk
banner; it now resolves like any read. Only read failures give up.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(editor): back off on superseded restore re-reads instead of capping them
A superseded read may predate the newer check, so it never resolves the
verification, even at the attempt cap. Overlapping checks now delay the next
fresh read (bounded, one timer per tab) and keep verification pending; only
failed reads count toward giving up.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(editor): address the restored tab by its owned id in the overlapping-check tests
After #31196 a server's tab is no longer keyed by its path, so these tests
flagged a tab that does not exist and no read was ever issued.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
All hosts gave every Orca server a 3 s scan, including the managed servers
Orca reaches through an SSH tunnel, which now serve every SSH host. A healthy
SSH host walking a large home timed out and showed up empty. The saved
environment's connectionDependency now picks the 15 s budget for tunnelled
servers; direct LAN/Tailscale servers keep 3 s.
Reported, with the proposed approach, by @quzard in #31139.
* fix(github): a twin project's PR cards, rows and dialogs act on the host that returned the item
* fix(github): key twin rows by host so cache sync keeps each copy; a lone row ignores the stamp
* refactor(editor): make the editor buffer record the source of truth for tab buffer state
Writers of the six buffer fields read and write the record, with the OpenFile
fields written as a mirror in the same store update. The store middleware no
longer copies tab values into the record; it only keeps records keyed by open
tabs (seeded from the tab on create, dropped on close) and, in dev and test
builds, asserts every mirror field equals its record. Host-mirrored web tabs
adopt the host's published values explicitly in the session sync patch.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(editor): check the buffer invariant in e2e builds and tidy the record helpers
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): give session-sync fixtures the now-required editorBuffers
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(native-chat): Codex chats list and run the skills Codex loaded
A Codex chat's skill list now comes from Codex itself (`skills/list`), live
while the chat runs and refreshed when Codex says its skills changed, and
from a short-lived Codex check while the chat is at rest. Plugin skills keep
Codex's `plugin:skill` names, disabled skills drop out, and each skill file
is its own row. A skill picked from the list reaches Codex as that exact
skill, beside the unchanged `$name` text, on hosts that take it.
* test(native-chat): account for Codex's skill list call; send without a trailing argument
* fix: align skill probes with chat launches and preserve picked mentions
* fix: distinguish skill choices in phone accessibility labels
* fix: clear picked skill files when queued text changes
* fix(native-chat): refresh resting commands when launch settings change
* fix: keep WSL agent refresh on its execution host
* fix: preserve skill selection at read and send boundaries
* fix: narrow agent identity for selected skill notice
* fix: use checked agent predicate for skill notice label
* test: retain locale in composer translation mocks
* fix(automations): a legacy automation on a twin project refuses instead of running on a guessed host
* fix(automations): capture a legacy run target before the dispatch snapshot, only where it keeps the folder
* fix(automations): re-read a legacy record after capture even when another run captured it first
* fix(orcad): replace a stale terminal daemon once it drains on Linux and WSL hosts
A daemon forked by an older orcad (before #31135, at umask 077) kept
serving new terminals after an upgrade if it was busy at startup, because
the runtime stale-bundle recycle was macOS-only. orcad now opts in on every
platform; the replacement still waits for exactly zero live sessions and
treats an unverifiable count as keep.
* test(daemon): pin the restart recycle test to linux; tighten wording
* feat(speech): set the OpenAI dictation key on a headless host (orca speech openai-key)
Cloud dictation needs an OpenAI key on the host that transcribes, and
desktop Settings was the only way to save one. A headless orca serve or
managed server had none. speech.openAiKey.{status,set,clear} save it
through the host's existing key store and secret sealing; the CLI reads
the key from stdin so it never lands in shell history.
* fix(cli): name the update when an older server lacks the speech key methods; one key length cap; PowerShell example (Opus review)
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* test(layout): terminal reply fixtures captured from the window-owned build, and main's switch-on proofs
* wip(layout): main's terminal surface index feeds the runtime graph (dark)
* wip(layout): main's panes stand in for the window's terminal list (dark)
* test(layout): the window sends main-listed panes as mount facts (dark)
* test(layout): surface proofs use public runtime calls
* chore: lint
* feat(layout): close by handle closes in main's layout; the close-time relist goes (dark)
* feat(layout): main's binding outranks a caller's pane claim on the terminal record (dark)
* test(layout): window-list tests stay on the rollback build; switch-on mount-fact cases (dark)
* wip(layout): main's terminal index reads E1's shared projection and follows its operation ends (dark)
* refactor(layout): the main-surfaces host lives beside the session-tabs readers, keeping the graph file under its line cap (dark)
* fix(layout): main's panes skip the window-era membership fence, follow workspace routing, and list a terminal when it registers; window close only hands the graph back (dark)
* refactor(runtime): explicit-close PTY stops move unchanged to their own module, for the file's line cap
* test(layout): the cold-restore table runs in a layout-owner build too: one push per registration
* test(layout): the parked and automation pane tests assert the same window report in both builds (mount facts with the switch on)
* chore: one layout-owner switch import after the restack on D1a
* feat(layout): main's terminal.list rows carry the host's verdict; a pane with a status is never live (dark)
* chore(layout): fit main's surfaces under the lint gates after the D1a restack
* feat(layout): every pane status D1a publishes keeps a row from reading live; remote host answers pass through (dark)
* feat(layout): a drag-out moves main's own binding and refuses the home copy's last pane; the window's terminal claim is not consulted (L127, dark)
* fix(layout): a tab close by a starting pane's handle stops the tab's terminals; a starting pane is never exited on the inventory alone; terminal.list lists every saved pane main owns; the pane claim lookup leaves the output path (dark)
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
* fix(layout): main's index reads routing only for a workspace two partitions hold, the fence reads ownership once, a legacy pane keeps its claim; switch-on tests assert main's answers after a window closes (dark)
* refactor(runtime): a worktree stop's terminal collection moves unchanged to its own module, for the file's line cap
* fix(layout): main's index routes every workspace as the layout stream does, re-reading routing only on a write or a terminal registration; a worktree stop names every terminal main binds; a starting pane is unverifiable (dark)
* feat(layout): every pane status main publishes keeps a row from reading live; a resuming pane is unverifiable like a starting one (dark)
---------
Co-authored-by: E2E Test <e2e@test.local>
Matrix the lane over distro families. Debian 12 and Fedora 41 rootfs are exported from their
container images (systemd off only, since the images ship no systemd). The lane test now makes the
systemd distro optional and writes its login-env export to the file a login bash actually reads
(~/.bash_profile on Fedora and Arch), which made new distros fail for the wrong reason.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(chat): preserve Claude rewind context after rollback
Fixes STA-11420.
* fix(lint): merge duplicate journal-types import
* fix(chat): skip torn Claude transcript lines when materializing a rewind fork
* fix(editor): name a server's file by its owned id from the first open, so it no longer reloads after a restart
* refactor(mirror): a server file's mirror id no longer needs the bare-path collision lookup
* fix(editor): conflict review reuses an open server file under its owned id instead of opening a second copy
* fix(editor): conflict review overview shows an open server file's unsaved edits under its owned id
* fix(editor): route conflict files by their workspace owner only; keep the overview lookup a plain read
* test(mirror): the browser-placement replay test names its server file by the owned id
* fix(codex): create Codex config.toml files owner-only
The runtime config mirror rewrote Orca's copy of config.toml (which carries
mcp_servers env tokens) at the umask default on every pass, and the trust
writer created a missing ~/.codex/config.toml the same way. New files now
start at 0600; the trust writer keeps an existing file's mode.
* test(codex): pick a kept mode that survives a strict umask
* fix(mobile): pick a cloud dictation model from the phone
The dictation setup sheet and Voice settings showed OpenAI models as
read-only text, so a host that lists only cloud models (Windows orca
serve, managed SSH or WSL servers) left the phone with nothing to pick.
A ready cloud row is now selectable through the same speech.dictation.setup
modelId write as a downloaded model; a cloud row without a key says to add
it on the paired computer instead of "Set up on desktop".
Refs #31174
Reported-by: delorean2020 (@delorean2020)
* style(mobile): template literal on the re-indented delete label
* test(mobile): complete the setup fixtures for the tests typecheck ratchet (Codex)
* fix(mobile): short not-ready label that cannot squeeze the model name; button roles and labels
* test(runtime): SSH probes answer over the Unix socket while the SSH layer is wedged (#31179)
* test(runtime): narrow parsed frames instead of asserting their type
* test(runtime): clean up temp dirs, tolerate socket resets in the SSH wedge guard
* test(runtime): parse wedge-guard response frames into a named type at the socket boundary
* test(runtime): say which frames the wedge-guard parser skips
* feat(ci): ratchet raw repo connectionId host reads; one drop-in resolver
Codex review 6 class fix (#4/#6/#7): host decisions read off a repo row's
legacy connectionId miss rows that name SSH only as executionHostId.
- getRepoRoutingConnectionId (shared/execution-host.ts): drop-in for a raw
repo.connectionId routing check that also finds executionHostId-only SSH.
- check-repo-host-read-ratchet: per-file count of raw repo connectionId
reads in src/main, src/shared, src/cli; counts may only shrink.
- Migrated 66 cheap reads: runtime GitHub/GitLab commands, PR/MR head ref
fetches, preserved-branch cleanup, ref queries, base-ref IPC, worktree
preparation, trash sweep, scan cache TTL, usage metadata.
* fix(ipc): PR base IPC routes executionHostId-only SSH rows; ratchet is a stated lower bound
Review follow-up (Opus on #31096):
- worktrees:resolvePrBase decided git/gh routing on the raw connectionId while
its head-ref fetches already used the resolver; one resolved connectionId
now drives all of it (test fails on main).
- getRepoRoutingConnectionId's contract is stated exactly: never local when the
raw field is set; on a contradictory ssh row executionHostId wins.
- The ratchet regex no longer matches report.connectionId; header and docs
say it is a lower bound.
- Regression tests for a runtime GitHub and GitLab command site.
* test(ci): pin the repo host-read ratchet step to the static phase
* fix(ci): repo host-read ratchet skips verbatim field copies into projected rows
A copy of connectionId into a projected row decides nothing; migrating it to
the resolver would change what the projection publishes. The sidebar feed
catalog's copy (added after the baseline) and two older projections no longer
count; baseline pruned to 91.
* fix(wsl-mover): match case-folded WSL paths in the exit proof, report panes without a restore folder, ignore stale agent rows
* fix(wsl-mover): attribute moved-folder shells by folder, recheck the fence after the census
* fix(orcad): run the remote daemon at the login umask so worktrees it creates are not owner-only (#31125)
* fix(orcad): keep refreshed Gemini OAuth tokens owner-only now that orcad runs at the login umask
The Gemini usage fetcher rewrote ~/.gemini/oauth_creds.json through a temp file and rename with
no mode, so the replacement took the process umask. Under the old inherited umask 077 that
happened to be 0600 on SSH hosts; at the login umask it would be 0644. Write it through
writeCredentialFileAtomic (0600 staging file, owner-only ACL on Windows).
The hostile-host matrix now checks that the deployed orcad runs at the SSH login's umask and that
its readiness file stays 0600.
* fix(orcad): keep rewritten agent configs at their mode and create the launch PID file owner-only
writeHooksJson now keeps an existing config's mode unless a caller opts out. Rewriting
~/.claude/settings.json (which can hold API keys) and the other agent configs through a temp file
used to take the process umask; inside orcad that was 077, and at the login umask it would widen an
owner-only file to 0644.
The launch runs `umask 077` in the foreground shell rather than inside the backgrounded nohup
list, so the PID file the foreground writes is created 0600 instead of relying on a racing chmod.
The Gemini credential test now pins umask 022 rather than inheriting the runner's.
* fix(agent-hooks): do not create a read-only temp file for a read-only Windows config
A preserved Windows mode can be read-only, which would leave an undeletable temp file behind when
the rename fails. POSIX keeps the preserved mode.
---------
Co-authored-by: Neil <neil@stably.ai>
* perf(persistence): persist a matching terminal binding without re-applying it
A reattach whose binding already matches memory but is not yet on disk now
only marks the selected session domain dirty and relies on the writer to
persist it, instead of cloning the whole session twice for rollback and
re-applying equal values. Adds a defaults-guard miss to the fast-lane
predicate and a binding.path span attribute.
* Preserve host ownership on durability-only PTY binding writes
* Inline PTY binding session dirty tracking
Invalidate cached catalogs on i18next resource additions and removals so plugin language-pack updates keep Experimental settings usable without restarting. Preserve cached values only after a successful rebuild.
Fixes#30781.
Contributor approaches reviewed in #30893 and #31190.
Co-authored-by: lorenzozanee <wyz0707@proton.me>
Co-authored-by: Chen <zwq19980411@gmail.com>
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a layout command that changes a pane's terminal raises its binding generation (dark, layout-owner build only)
* fix(layout): main shows unshown live terminals again when a client attaches or the terminal service is replaced (dark, layout-owner build only)
Headless servers had no retry: a terminal the startup pass declined stayed tabless until the
next restart. The pass now also runs when a client authenticates and after the local terminal
provider is installed or replaced, one pass at a time. A terminal whose pane record fails to
save after main bound it is no longer reported as left without a pane.
* feat(layout): main stores each pane's last grid on its tab layout (dark, layout-owner build only)
A new main-only setPaneGrid command records the grid a pane's terminal was last sized to, on
its leaf. It is saved as the layout's optional lastGridsByLeafId, which older builds drop on
read, so a downgrade loses only the hint. It moves with its pane.
* feat(layout): main remembers a resized terminal's grid on its pane and starts the pane at it (dark, layout-owner build only)
Each accepted resize, from a window or a client, is written once per burst as the pane's last
grid. A pane main holds starts at that grid, after a size the caller measured and before its
group's size, then 120x40.
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's grid resized just before quit is saved, and a grid change publishes nothing (dark, layout-owner build only)
The desktop and serve quit and orcad's shutdown write any grid still inside its one-second
burst before the final store flush; a quit with none waiting adds no await. A grid is main's
own record: it is not in a published pane, so a resize burst sends no layout frame, and its
write patches only the pane's tab.
* feat(layout): a pane's status names sleeping, resuming and held, and what a status on a bound pane means (dark, layout-owner build only)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): main starts fresh a restored pane bound to another workspace's terminal (dark, layout-owner build only)
The window's restore rule, now in main for every host: a pane whose stored terminal id was
minted for another workspace is unbound, so it starts fresh, and that terminal, if live, is
placed in its own workspace. The id check moves to shared, which the window now imports.
* fix(layout): a start refused because a newer build's terminal service may still run the pane shows it held, not failed (dark, layout-owner build only)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* feat(layout): main's one start of a restored pane with no live terminal, shared by every non-window trigger (dark, layout-owner build only)
Restores from the pane's own stored terminal and resumes a local agent record; concurrent
triggers share one start, and a start that lost to another starter resolves to the winner's
terminal. Not wired yet.
* feat(layout): a client's first view and a CLI start of a restored pane go through main's one starter (dark, layout-owner build only)
Twin j: the phone's first-view start of a pending row moves into its own runtime layer and,
in the layout-owner build, runs through RestoredPaneStarter, which A1's send, wait, read and
wake call as ensureRestoredPaneStarted. It restores from the pane's own stored terminal, resumes
the agent the host recorded there, and resolves once the terminal is registered. With the switch
off, the phone's start is unchanged.
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* test(layout): a window restore, a client first view and a CLI send of one restored pane start one terminal (layout-owner build)
* fix(layout): the restore pass releases a pane from another workspace's terminal through main's layout command, and two files stay under the line limit (dark, layout-owner build only)
A new main-only releasePaneTerminal command unbinds a pane from the terminal it names and stops
nothing, so the pass writes no layout outside a command. The stored terminal layout schema moves
to its own file, and a pane's own size (measured, else its last grid) is one helper.
* test(layout): drop a needless spread in the pane release test
* feat(layout): main shows a restored pane whose terminal is gone as sleeping, and a window treats a status-only frame as a binding change (dark, layout-owner build only)
After a census that answered, each local pane bound to a terminal the host no longer runs gets
status 'sleeping' and keeps the id it restores from; any bind ends the status. Relay and paired
server panes are their own host's to judge. A window rebuilds nothing for a frame that only
changes a pane's status.
* test(layout): restored history reaches each start across two host restarts with no client acking the cold restore
A guard, not a fail-first test: the cold-restore ack only drops the daemon's in-memory copy
(daemon-pty-session-control.ts) and the re-anchored checkpoint carries the history
(daemon-pty-spawn-result.ts), so a host with no client keeps it. C1c moves the ack into main.
* test(layout): the activation gate and worktree-reopen tests assert the same behaviour through main's command and frame (layout-owner build)
In a layout-owner build main places live terminals and draws them through its frames, so the
gate's adoption tests now check the window makes no tab or binding of its own, adopts only what
main's frames hold, and resumes no agent over a live terminal main has not placed yet. The
worktree-reopen tests feed main's frame for the restored pane and answer layout.apply, checking
one tab bound to the live terminal and one resumed tab across reopens.
* fix(layout): a restored SSH pane reattaches over its relay, and the restore starter returns the terminal it started (layout-owner build)
An SSH terminal id names its connection, not a workspace, so the rule that drops another
workspace's terminal no longer releases it or starts the pane fresh. The starter now resolves
to the id its start registered instead of re-reading the stored layout, whose bind write can
still be in flight, and a tab saved with no layout counts as one held pane.
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(layout): the restore starter's start result types a missing terminal id as none
* fix(layout): a workspace's first tab waits for main's placement of its live terminals, so activation opens no extra shell beside one (layout-owner build)
A window activating a workspace before main's placement pass had read the host census found no
tab and asked main for the first one; main admitted it into the still-empty workspace, then the
pass added the live terminal's own tab beside it. An initial create now waits out a pass in flight.
* fix(layout): the restore starter never resolves to another workspace's live terminal, and a first view of a pane main dropped is refused (layout-owner build)
* test(layout): the first-tab placement test overrides the worktree map with its declared type
* fix(layout): the first-surface wait lives with main's placement pass and keeps the runtime file under the line limit
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(layout): a workspace's first surface waits only for the placement pass running when it arrives, and the switch-off build applies commands as before
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(layout): a quit's pane-grid writes never hold the stop past one second, and orcad awaits nothing for them when none wait
The grid flush before the final save (desktop and serve quit, orcad stop) is now raced against a
1 s budget; a grid it misses only means that pane restarts at the default size. With the switch
off no grid is ever recorded, so the flush is null and orcad no longer awaits even that.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* fix(layout): a quit's grid flush swallows its own failures, keeps its budget timer live, and the quit's no-await note names it
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
* fix(layout): a quit writes pending pane grids before the SSH shutdown, and a workspace's first tab waits at most 0.5 s for main's placement pass
The quit's grid flush (still capped at 1 s) now runs before beginSshShutdown, so nothing is awaited
between it and the final save; with no grid waiting, both start synchronously as before. A first tab
waits for the running placement pass at most 500 ms, then is admitted. The quit test's SSH mock now
names the module the quit imports.
* test(layout): the quit tests prove the final save starts in the same turn as the SSH shutdown, in order after the PTY kill
* chore: keep main's bytes for files the merge's format hook rewrote (native-chat formatting is #31184's)
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(orcad): give the headless server a stats collector, and make the parity test see positional and host-port wiring
Mobile home on an orca serve host showed 0 agents and 0 agent time since
1.4.229: orcad built the runtime with no stats collector, so stats.summary
answered {}. orcad now loads the profile's orca-stats.json and counts hook
sessions through the same feed the desktop uses.
The runtime composition parity test only compared constructor options and
runtime/rpc setters in four files, so a positional constructor argument
(stats) and process-wide host ports installed at preflight (speech) were
invisible to it. It now records both, and checks every declared
construction option is accounted for.
Reported-by: Fergus Molloy (@fergusmolloy)
* fix(orcad): subscribe stats before hooks start; parity test ignores teardown resets and finds unlisted host-port files
* fix(native-chat): name a chat from its first message at once; the generated name replaces it
A native chat's one stored name was written only by a best-effort, one-shot
generation, so any failure left every surface on the 'Codex Chat' placeholder.
The first prompt now names the chat when its first turn starts, and the
generated name replaces it only while the field still holds that seed.
* fix(native-chat): the chat names setting says a chat shows its first message until it is named
* fix(native-chat): rewind works at any chat length
Rewind copied the kept part of the chat into its saved record and, for
Codex, re-read and re-translated the whole provider history before and
after the revert. Both were bounded (10,000 rows / 2 MB host, 1,024
entries / 2 MB Codex), so long chats were refused with "too long to go
back in".
The record now names the cut; the kept rows are derived from the
journal, which keeps the old epoch until the rewind completes. Codex
reads only the turns it undoes and trusts thread/revert; recovery
proves an applied revert from the newest turn alone and never
truncates otherwise.
* feat(native-chat): Claude chats launch a rewind's fork of the kept conversation
A rewind record can name the provider session the chat continues in (providerFork). Claude's next
launch forks it with the SDK's own truncating resume at the rewound prompt's parent, and publishes a
forked link once the provider has created it. Removes the transcript proof branch that could never
pass on the real CLI.
* test(native-chat): pin Claude's rewind fork against the real CLI
* feat(native-chat): rewind Claude chats
Claude declares rewind. The adapter answers where a rewind forks (the rewound prompt's parent in
its own transcript); the host saves that fork with the prepared record, stops the agent, and
rebuilds the journal from the rows before the message. Recovery completes a prepared fork, since
nothing outside Orca can have half-happened, and the next launch makes the fork.
* test(native-chat): keep the Claude rewind tests to the flows they pin
* test(codex): type the rewind fixture's page params instead of asserting them
* ci: run the PR checks against main
* fix(native-chat): rewind recovery reads the provider handle only from a record that exists
* fix(native-chat): import the session record type once
* test: run the rewind cut test in the SQLite runtime project
* test(native-chat): run the fork rewind test on SQLite, and give Claude's test double its fork
* fix(native-chat): omit rejected submissions from rewound history
* fix(native-chat): rewind forked chats without restarting the agent
* fix(native-chat): Claude rewinds also leave out messages that never ran
* fix(native-chat): Claude rewind to a mid-turn message drops the rest of that turn
A message sent while Claude is answering joins the running turn. Rewinding to it
forks Claude's conversation at that message, so the chat now drops the rest of
that turn too instead of keeping replies Claude no longer has.
A rewound chat whose kept conversation lives only in another Claude account now
gets the same "history is in another account" refusal as a resume, instead of
Claude's raw start failure on every launch.
* fix(native-chat): rewind only to messages that start a turn
A message sent while the agent is still answering joins that turn. Claude's CLI
holds it until a break and records it after the reply; Orca's chat records it
when it was sent. Rewinding to it therefore cut the chat and Claude's
conversation at different places: the previous fix dropped reply text Claude
still remembered, and the version before kept the message itself. For Codex the
cut started at the turn's own first message, removing a message the user did
not pick.
Rewind now returns only to messages that opened their own turn, the common
pattern: the chat stops offering it on a message sent mid-reply, and the host
refuses one, so a mid-reply message and its turn are always kept or dropped
together. The Claude cut goes back to keeping a kept turn's late rows.
* fix(native-chat): rewind targets follow the turn record's opener; saved rewinds still complete
A message opened its turn when the turn record names it, not only when it sits
at thread level: restored history scopes its openers to their turns, so those
chats lost Rewind.
The opener check now runs only when a new rewind is admitted. Recovery cuts
wherever an already saved rewind points, so one saved by an earlier build
still completes instead of blocking the chat.
* fix(native-chat): recovery's read-ahead reads turns only from a Codex rewind cut
* test(orcad): skip processes older than their claimed parent in the Windows daemon teardown walk
Windows keeps an exited parent's pid in its children's ppid and reuses pids,
so system processes and leftover build helpers could be walked as daemon
descendants and fail teardown. A row created before its claimed parent is now
skipped; a real child the daemon left running still fails teardown.
* test(orcad): skip children of a process that reused the daemon pid; name ppid and age in the leak error