Commit Graph
14360 Commits
Author SHA1 Message Date
OrcaWinandOrcaWin a3dda8af42 perf(typecheck): check shared tests only in the Node project (#29381)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 20:52:31 -07:00
OrcaWinandOrcaWin 79d131d521 ci: move the final PR report off the busy runner pool (#29337)
* ci: report final PR checks outside the shared runner queue

* ci: retain the mobile harness test mock allowance

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 20:48:39 -07:00
Jinwoo Hong 9b99928cb5 fix(layout): close the dark layout path's gaps, and refuse to package it (#29207)
* feat(layout): add the dark layout.apply channel and route pane splits through main behind ORCA_LAYOUT_OWNER

A build made with ORCA_LAYOUT_OWNER=main registers layout.apply: main applies the
window's layout command to the stored partition through the shared applyLayoutCommand,
minting new ids. A user split then waits for main's answer and mounts the pane id main
minted. Every other build substitutes 'window', so neither branch is compiled in.

* fix(layout): write only the records a layout.apply command touched

A load-then-save rewrite also normalizes records the command never touched. The command's
effect is now the diff between the save before and after it, written onto the stored
partition record by record. A split whose main reply says a paired server holds the tab
splits as today.

* fix(layout): write a load-supplied record whole and check that patched partitions reload to the command's result

* feat(layout): close, rename and equalize panes through main behind ORCA_LAYOUT_OWNER

In a layout-owner build the window's pane close, title change and equalize go through
layout.apply and the window shows main's answer. A window pane close keeps the legacy
close's rules: the tab's last pane is refused, a pane restarted since main read the close is
left alone, every partition holding the tab closes it, and the tab's remote session id follows
the remaining pane. The rules sit behind a command field only layout.apply sets.

* fix(layout): close the tab when a racing pane close leaves the window one pane

* fix(layout): leave an unmounted tab alone when main refuses its last pane

* feat(layout): move panes and save divider ratios through main behind ORCA_LAYOUT_OWNER

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): a taken pane drop whose pane closed does nothing

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the window's pane move to main's saved tree, and the drops main cannot take

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): close a user's pane even if its terminal restarted, as today

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the restarted-pane close directly, not through a made-up profile state

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): leave a split to today's path only when the workspace's row names a paired server

The split switch point asked which host to route to with a helper that falls back to the focused
server, so a focused server could claim a workspace whose row names no owner. Use the same owner
test the tab mirror uses.

* test(layout): type the split owner mock without a cast

* test(layout): type the layout-owner command mocks with the real signature

* test(layout): type the gesture command mock with the real signature

* fix(layout): close the dark path's gaps: unmounted split panes, refused or failed splits and moves, ratios during a move, release guard

- A pane main saved but the window could not mount (source pane or manager gone during the await)
  is closed again in main, so it does not come back on restart.
- A split main refuses because its copy lacks the workspace, tab or pane, or a failed call, splits
  as today; a refused or failed move moves as today.
- Divider ratios committed while a move is in flight wait for the window's move, then go to main.
- The orcad bundle gets the ORCA_LAYOUT_OWNER define when the switch is on.
- Packaging and the orcad template refuse ORCA_LAYOUT_OWNER=main and any output a dark build marked.
- workspace-layout-record-patch.ts: no non-null assertions; plain comment for the map depth.

* test(layout): count a split pane as mounted from the split mock's own results

* fix(layout): act as today whenever main lacks the target or the call throws, not just for splits

- sendPaneCommandToLayoutOwner runs asToday for workspace/tab/pane_not_found and a thrown call, so
  close, rename and equalize fall back like split and move; a handler that throws no longer runs the
  fallback too.
- A close main refuses as its last pane, while this window still has others, closes as today.
- A move whose local step throws still releases held divider ratios.
- Drop the orcad output marker: nothing read it; the template build refuses the switch itself.

* test(layout): plain names for the last-pane fallback test and the marker comment

* docs(layout): say plainly what an indeterminate write can leave behind

* fix(layout): finish the main merge with this branch's versions of the layout-owner files

* test(build): copy the layout-owner build guard into the compiler-cache checkout fixture

* fix: carry this branch's build-guard changes onto main's versions of three build files
2026-10-15 20:16:58 -07:00
Neil 82a2414c53 ci: allow the shared MobileNativeChatView test harness to register module mocks (#29378)
#29368 moved these hoisted mocks from the test file into a shared fixture, which the
anti-slop no-module-mocking rule only exempts for *.test files. Scope the same
file-level exemption the RPC test fixture already uses.
2026-10-15 20:15:05 -07:00
Neil e554ef2a78 ci: cancel obsolete PR checks even when new checks are skipped (#29364)
* ci: cancel superseded PR checks even when replacement checks are filtered out

* test(ci): verify cleanup keys against every automatic PR workflow
2026-10-15 20:12:36 -07:00
Neilandmmarabel a49295f024 fix(sidebar): Hide sleeping keeps a live workspace row through a transient PTY gap (#29325)
A pane that rebinds its PTY clears the tab's live binding one commit before
re-registering it. Under Hide sleeping an open-but-idle remote workspace hung on
that binding alone, so its row was swept out and back for one frame. The sweep
now treats a still-open tab with no binding as unverifiable and keeps a row that
was live for a short settle window; a deliberate sleep or a closed last tab
still sweeps immediately.

Fixes #15996

Co-authored-by: mmarabel <166927047+mmarabel@users.noreply.github.com>
2026-10-15 19:51:32 -07:00
Neil 85121dd4d0 fix(ssh): no probe stall on the first remote action after a pause (#29240)
* fix(ssh): skip the managed-server probe while the control socket is answering

A remote call after more than 5 s idle first ran a status.get probe on a new one-shot socket (SSH channel, WebSocket upgrade, hello, auth, request). A ready shared control socket that heard from the server in the last 20 s already proves it serves; a kill, reboot, rebind or stop closes that socket, so the probe still runs then.

* fix(ssh): read control-socket answers through a leaf module

orcad-managed-tunnel imported the request-connection cache, which (through the status owner) imports the serving gate back. Tests that mock that cache with an async factory deadlocked importing it. A leaf module now carries the answer source between them.
2026-10-15 19:50:27 -07:00
Neil 330fd55376 test(mobile): split MobileNativeChatView.test.ts under the max-lines limit (#29368)
Pure move: the 36 test cases and their assertions are unchanged, now in six feature files.
Shared mocks and render helpers live in MobileNativeChatView.harness.test-fixture.ts. The only
non-move edits are typing and formatting: the harness adds getComposerEditGeneration to its
default props (a required prop the fixture never passed), tests use hostType(node) for tag
comparisons, and the typecheck baseline and reliability gate paths drop the removed file.
2026-10-15 19:49:38 -07:00
OrcaWinandOrcaWin bd56d8d096 perf(dev): skip unchanged process-host package rebuilds (#29317)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 19:45:12 -07:00
Neil fe08eb9a23 fix(agent-status): stop nested reviews from collapsing Claude children (#29264)
* fix(agent-status): preserve parent status through nested review hooks

* fix(agent-status): preserve relay handoffs and verify session consumers

* fix(agent-status): scope nested provider guards to terminal owner
2026-10-15 19:44:37 -07:00
NeilandClaude 7306273d89 fix(ssh): switching back to a quiet remote terminal no longer refetches its snapshot (#29253)
* fix(ssh): switching back to a quiet remote terminal no longer refetches its snapshot

Co-Authored-By: Claude <noreply@anthropic.com>

* test(cross-version-wire): drop notice negotiates only between current peers

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 19:39:59 -07:00
OrcaWinandOrcaWin 82029e6908 ci: stop warming unused Windows package archives (#29322)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 19:38:23 -07:00
Jinwoo Hong 118cbd0481 feat(layout): move panes and save divider sizes through main behind ORCA_LAYOUT_OWNER (#29192)
* feat(layout): add the dark layout.apply channel and route pane splits through main behind ORCA_LAYOUT_OWNER

A build made with ORCA_LAYOUT_OWNER=main registers layout.apply: main applies the
window's layout command to the stored partition through the shared applyLayoutCommand,
minting new ids. A user split then waits for main's answer and mounts the pane id main
minted. Every other build substitutes 'window', so neither branch is compiled in.

* fix(layout): write only the records a layout.apply command touched

A load-then-save rewrite also normalizes records the command never touched. The command's
effect is now the diff between the save before and after it, written onto the stored
partition record by record. A split whose main reply says a paired server holds the tab
splits as today.

* fix(layout): write a load-supplied record whole and check that patched partitions reload to the command's result

* feat(layout): close, rename and equalize panes through main behind ORCA_LAYOUT_OWNER

In a layout-owner build the window's pane close, title change and equalize go through
layout.apply and the window shows main's answer. A window pane close keeps the legacy
close's rules: the tab's last pane is refused, a pane restarted since main read the close is
left alone, every partition holding the tab closes it, and the tab's remote session id follows
the remaining pane. The rules sit behind a command field only layout.apply sets.

* fix(layout): close the tab when a racing pane close leaves the window one pane

* fix(layout): leave an unmounted tab alone when main refuses its last pane

* feat(layout): move panes and save divider ratios through main behind ORCA_LAYOUT_OWNER

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): a taken pane drop whose pane closed does nothing

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the window's pane move to main's saved tree, and the drops main cannot take

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): close a user's pane even if its terminal restarted, as today

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* test(layout): pin the restarted-pane close directly, not through a made-up profile state

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125

* fix(layout): leave a split to today's path only when the workspace's row names a paired server

The split switch point asked which host to route to with a helper that falls back to the focused
server, so a focused server could claim a workspace whose row names no owner. Use the same owner
test the tab mirror uses.

* test(layout): type the split owner mock without a cast

* test(layout): type the layout-owner command mocks with the real signature

* test(layout): type the gesture command mock with the real signature

* fix(layout): finish the main merge with this branch's versions of the layout-owner files

* chore: keep main's exact bytes for files this branch does not change
2026-10-15 19:36:23 -07:00
Brennan Benson 7a78dbd3c7 Native chat: one settlement transaction for everything an ended run left (4/5, rebuild of #26867) (#28111)
* feat(native-chat): record which agent run wrote each journal item

Each journal item row, subagent roster entry and background-task entry now
records ownerFence: the lease fence of the agent run whose output produced it.
Optional, persisted, no row version bump and no migration. The journal's
itemFence() returns it, so the turn-verdict rules and per-run provider activity
read the producing run rather than the writer's fence.

* feat(native-chat): one current-work projection for every host reader; record death first

The host decides what work is current in one place
(structured-agent-session-current-work.ts) from the lease, the exit this host
saw, the runtimes it replaced, its live child and each item's provenance.
Every host reader (queue, Stop, prompt answers, commands, status feed, idle
sweep, teardown snapshot, history pages and frames, orchestration mailbox)
reads it, so a dead agent's leftovers stop reading Working or holding the queue
even when their cleanup write failed. The journal's activeTurnId()/liveTurnScope()
become the raw runningTurn().

On exit the host records the child's end first and signals generationEnded,
which republishes frames, the status feed and settlement waits and wakes the
queue drain with no journal row. Pages and frames carry optional working and
actionablePromptIds fields.

* feat(native-chat): clients read the host's working answer; refuse answers to a stopped agent plainly

Desktop and mobile prefer the host's working and actionablePromptIds fields
when present (merged across batches like latestTurn) and fall back to today's
row derivation against an older host. A prompt an ended agent raised stops
looking active; answering it is refused before any provider sees it, with
"<Agent> has stopped, so your answer was not sent. Send a message to continue."
Cancelling it only dismisses it in the journal: it no longer triggers the
chat's Stop, pauses the queue or withdraws queued cards.

* feat(native-chat): one settlement transaction for everything an ended run left

An exit now settles, in one lifecycle batch planned at its own turn in the
write queue, everything its run left (turn, calls, prompts, reasoning,
unanswered sends) plus leftovers of earlier ended runs, and only the subagent
roster entries and background tasks those runs observed, by each entry's own
provenance. Main's stale settle moves to stale-state-settlement.ts with fence
scoping; dead-generation-settlement.ts becomes a planner;
leftover-settlement.ts combines them.

Resume eligibility widens in one case: this host saw the root exit at the
lease's fence and the release write failed; the acquisition replaces the
owner only once a probe proves it gone.

* refactor(native-chat): keep structuredAgentSessionHandsOver file-local until a caller needs it

Nothing outside the projection calls it in this part; Part 5 exports it for the start-failure row.

* test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case

Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one).

* test(native-chat): drop current-work mirrors: the viewKey unit case and the still-live rewind case

The flow cases keep every outcome: dead leftovers read as nothing, Stop has nothing to stop, a waiting card drains, a live owner publishes as before, and a rewind never promotes dead work.

* test(native-chat): fewer client tests: one flow per client, one fallback each

Keeps desktop and phone Working from the host (with the older-host fallback), the reducer merge contract, the ended question Cancel that only dismisses, the agent-named refusal and the resolution receipt. Drops per-helper prompt tests, the transcript-placement test, the stop-note client reduction, the Steer cards that match main, and per-reason refusal rows.

* test(native-chat): fewer settlement tests: keep the exit-delivery flows and the per-entry scoping flow

Drops the no-added-delay timing check, the frame-once check, two stamping unit cases and the Codex combined case (the roster flow test covers its outcome).

* test(native-chat): drop the resume-eligibility predicate mirror; move the unstamped-entry case to Part 1

* test(native-chat): an unstamped roster entry from an older build reads as its row provenance

Moved here from Part 4 so it sits beside stampJournalEntryProvenance.

* test(native-chat): build the older-build roster row inline, after main removed its body helper

Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads.

* refactor(native-chat): provenance is per item only; drop per-entry stamping

Main's #27947 writes one saved row per subagent, and each background task already has its own row,
so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps
inside a row mattered only for the shared roster rows saved before #27947, and no run writes into
those any more, so the row's own provenance settles them correctly. Removes the entry stamping,
the optional ownerFence on roster entries and background tasks, and their tests.

* fix(native-chat): an item's provenance is set when it is created; revisions keep it

A newer run's provider can revise an item an older run created: Pi's start and an ACP
session/load both report context usage onto the newest saved turn. Restamping that turn with
the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted.
Only creation states an owner now, as main's itemFences rule did.

* test(native-chat): keep the provenance test below the production lines it covers

* fix(native-chat): an ended agent's card Cancel commits with its receipt, through dismissPrompt

The ended-generation dismissal wrote the cancelled resolution without the Cancel's operation
receipt and without the dismissal's failure handling, so a receipt write failing after it would
answer "outcome unknown" over a card already closed. It now goes through dismissPrompt, which
carries the uncommitted receipt in the same append and answers dismissalNotSaved on failure; a
null `answer` skips the adapter, since no live provider holds an ended generation's card (Pi's
adapter refuses a fence with no live session outright).

* Native chat: a resumed subagent or Claude background task belongs to the run executing it

A subagent row that run 1 created and run 2 resumed kept run 1's creation
fence, so settling run 1's death marked run 2's live subagent unverifiable.
Claude background tasks reuse their rows the same way (a matching parent tool
use, or a parentless row).

Rows of reusable child execution now also name their execution owner: the
fence of the child process whose report the write copies, captured when that
report arrives, never the lease that is current when a queued write flushes.
- Subagent rows: the host captures the live child's fence when its child-work
  report changes the records, and rewrites a running subagent another child now
  reports even when its text is unchanged.
- Claude background-task rows: written through that child's own event sink,
  which stamps its bound fence.
- Creation provenance (`ownerFence`) is unchanged. Writes that do not state an
  owner (settlement, Pi's start usage, ACP session/load replay, ACP tasks) keep
  it, and a row without one falls back to its creation fence.

`journal.itemFence()` now answers whose execution an item is
(`executionOwner ?? ownerFence`), so every settlement and liveness read, inside
its write transaction, follows the run that resumed the work.

Also drops the event sink's compatibility alias for the identity resolver (the
Codex sink names the resolver itself), keeping the sink under its line limit.

* Native chat: a subagent row is written through the child whose report it copies

Review of c0add67cca: the subagent row writer read its execution owner from
the host's installed child when a report landed. A child that is still being
acquired reports a resumed subagent before the host installs it, so its row
was written at once under the old owner, then the attach's settlement of what
the previous child left marked it unverifiable, and nothing rewrote it until
the next report.

Each adapter (Claude, Codex, ACP) now hands its child-work report to the host
with that child's own event sink, the write path its rows already take. The
host passes it through the status feed to the subagent row writer, which writes
the rows that report changes through that sink with `executes`. They land in
the child's own order, after the attach's settlement, at the fence the sink is
bound to, and that fence is the row's execution owner. A running subagent a
child has not written yet is written once even when its text is unchanged.
Changes the host makes itself (a Stop's acknowledgement, an end) still write
directly and keep the row's owner. The shared child-work evidence type is
unchanged: the sink travels as a separate argument.

Test: a Claude helper the next child reports again while it is still starting
stays working after that start's settlement, through the shipping adapter and
host. It fails on c0add67cca (the row lands `unverifiable`).

* Native chat: the subagent row writer always awaits a promise

CI's type-aware lint flagged the row writer handing `Promise.all` a value
that could be undefined when a sink has no `written()` barrier. A sink without
one now reads as landed.

* Native chat: keep refused subagent writes, and carry both owners through a rewind

Review of b161aa38b1 / 129f36d49b:
- `admitChildWork`'s producer is optional again, so the host's own
  turn-started report compiles.
- A subagent row written through the reporting child's sink uses the sink's
  checked admission. A write it refuses under backpressure is queued again
  once what the sink holds has landed; a refusal or a failed write frees that
  child's claim on the row, so a later report retries it.
- A rewind carried `itemFence()` (now whose execution an item is) into the
  rebuilt row's creation `ownerFence`, and dropped `executionOwner`. Retained
  rows now carry both as stored (`journal.itemProvenance`), the rewind record
  keeps `executionOwner` beside `ownerFence`, and both rebuild paths write it
  back: epoch replacement and the context rewind, which builds its rows from
  the same replacement items.

Test: the two-fence test now rebuilds the epoch from the resumed rows and
settles the first run's death again; the resumed rows stay live. It fails if
the epoch replacement drops `executionOwner`.

* test: run the resumed-child owner test in the Node SQLite project

It opens a real journal database, so it belongs on the list the SQLite runtime
boundary test checks.
2026-10-15 19:35:36 -07:00
Neil a54236751e fix(serve): start Linux headless serve on X11 so an inherited Wayland session can't abort it (#29260)
* fix(serve): pin headless Linux serve to X11 so an inherited Wayland session can't abort startup

Fixes #25400

* fix(serve): put --ozone-platform=x11 on the launch argv for Linux headless serve

Chromium caches its Ozone platform in PreSandboxStartup, before main JS runs,
so app.commandLine changes cannot move it off Wayland. Pin X11 on the CLI and
orcad serve spawns, and re-exec in-process serve (same pid) when Electron
already resolved Wayland.
2026-10-15 19:30:36 -07:00
Neil 2e510c0cb2 perf(remote): send one changed tab instead of the whole worktree on session tab streams (#29319)
Fixes #22151
2026-10-15 19:28:44 -07:00
Neil 6578f0b3a9 test(git): move worktree cases out of the Git binary compatibility test (#29357)
Pure move to bring the file under the max-lines limit; same cases in the same order.
2026-10-15 19:28:30 -07:00
Jinwoo Hong 7538fa9466 feat(mobile): the phone's Workspace options sheet, with every desktop option (#28997)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters

A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.

* test(e2e): dedupe sidebar row ids in the visibility spec

* fix(mobile): address review: owner naming, old-server relayed phones, scope trim

* refactor: move the sidebar's project section grouping into shared

The project sections the desktop sidebar draws under Group by Project are
built from the project/setup projection, the per-host catalog fold, the
endpoint host stamp, and the section labelling and ordering rules. Move the
pure parts of each into src/shared (no behaviour change) so the paired phone
can call the same code instead of guessing the grouping.

* fix(mobile): group by Project with the desktop sidebar's own sections

The phone grouped Project sections by repo display name, so it merged
checkouts the desktop keeps apart (several local stablyai/orca checkouts,
or a server's copy) and split what the desktop merges. The phone now reads
each host's repo.list, project.list and projectHostSetup.list (the desktop's
own and each server's through the relay), folds them with the renderer's
shared catalog fold and projection merge, and builds sections with the
desktop's shared grouping, labelling and ordering code. Keys, labels and
Recent order equal the sidebar's; a host that predates the project lists
gets the repo-derived projection, as the desktop does.

Header colour and icon come from the section's anchor repo and rows from
their own repo by id, as on the desktop. The repo filter lists every
catalog repo by id per host instead of merging same-named repos. Group-by
labels read Project and None. project.list and projectHostSetup.list are
routed for paired phones (read-only, workspace permission).

* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag

A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.

* test(e2e): poll the phone and the sidebar together in the visibility spec

* fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order

Review fixes. The sidebar's buildOrderedGroups now calls the shared
buildProjectSections for Group by Project (notice and pending repos are
optional inputs), so desktop and phone run one function. The phone ranks
manual header order by its folded catalog order, which is each host's
repo.list order with the desktop's own repos first, as the renderer loads
them; only the desktop's cross-host manualRepoOrder (pairing-local) is
unknown to it. The projection selector is now a memo over the shared
resolver. A transient project-list failure keeps that host's last catalog;
only forbidden or method_not_found falls back to the repo-derived
projection. Catalog rows are checked on every member grouping reads, and
the parity test answers the renderer's real catalog fetch, includes a
server's own project rows, and fixes Manual and Recent orders that differ.

* fix(mobile): use the repo-derived catalog until a host's project lists first answer

* fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it

The phone fetched repo.list, project.list and projectHostSetup.list from
the desktop and every relay-ready server each minute and folded them
itself, so rows from offline or older servers fell back to repo sections
and keys moved on cold start.

The desktop renderer now publishes the inputs its own Project headers use
(state.repos in sidebar order, the projection selector's projects and
setups, projectOrderBy) on the runtime window graph it already sends to
main, only when they change. Main keeps the last one and answers it on a
new paired-desktop read, mobileRelay.projectSections. The phone reads it
with its repo metadata and groups with the shared builder; a desktop that
predates the read (forbidden or method_not_found) leaves the phone grouping
rows by repo from the desktop's repo.list, and a transient failure keeps
the last catalog. The payload carries only grouping and label fields, no
hooks, credentials or uploaded icons. The phone's own catalog fetching and
its project.list/projectHostSetup.list routes are removed.

* fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests

* test(e2e): the desktop window serves its sidebar's Project catalog to the phone

* chore(i18n): regenerate the runtime English catalog (drift already on main)

* fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow

The extra read inside the repo-metadata refresh stalled its recorded RPC
sequence. It now runs beside it with its own throttle. The repo-metadata
goldens are re-recorded for the intended state change: the name-keyed
colour, icon and id maps became desktopRepos.

* refactor(sidebar): one shared Workspace options filter and sort evaluator

The sidebar, the jump palette and the options badge each applied the
Workspace options filters with their own copy of the rules. They now
call one pure evaluator in src/shared/workspace-view, which reads each
row through a small fact reader so the phone can call it next with the
facts its host serves. The sort comparator moves there too, with each
surface supplying its own Agent activity rank.

No behaviour change: a parity test runs the old sidebar chain and the
new evaluator over every toggle combination, and the old comparator
against the new one for each sort mode.

* wip: workspace view descriptors

* refactor(sidebar): address review of the shared Workspace options evaluator

One predicate builder instead of a per-row scope argument; one
exemption-narrowing function taking the hide flag; consumers import the
shared predicates directly; Agent activity ties fall to the label in the
shared comparator. The activation filter lift reuses the shared
predicates. The parity test now covers other-client rows, live agent
attention classes and the sort-label map.

* fix(mobile): fetch the Project catalog at once when the screen moves to another host

The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers.

* wip: desktop option items from shared choices

* refactor(sidebar): the Workspace options menu renders from one shared option list

src/shared/workspace-view/workspace-view-options.ts lists every Workspace
options control (Hosts, Projects, Group by, Sort by, Project order, Card
layout, Show properties or Card display, and each filter) with its
section, its English copy, its choices and when it shows. The desktop
menu walks that list and renders each option through a table keyed on
the option key, so a new option fails to compile until the desktop has
a control for it; the phone sheet will do the same.

The choice lists (sort modes, project order, card layout, agent activity
layout, card properties) are derived from the shared ids, and a test
checks the desktop's English copy matches the shared copy. The unused
classic branch of the card property list is removed.

No visible change.

* wip: row facts

* refactor(sidebar): address review of the shared Workspace options list

The show rules get their own tests with fixed expectations (the cases
the old filter-section test covered, plus the rest), and the menu test
checks each option's own control rather than any matching text. Card
display rows toggle one property each, with the folder-path copy in
shared. The unused Status and Tasks copy left by the removed classic
card list is deleted from the catalogs. Filter rows write through the
store directly; files are named for what they hold.

* feat(runtime): worktree.ps carries the Workspace options row facts

Each worktree.ps row now says whether it is the project's default-branch
workspace, created by an automation, created by the CLI, or on a
detached HEAD, answered by the same shared predicates the desktop
sidebar filters with. The phone has none of these facts today, so its
copies of the filters either guess (default branch) or are missing.

Additive and inert: optional fields that nothing reads yet. An older
host omits them; readers must treat absence as 'not that kind', so the
row stays visible. Server rows relayed through the desktop pass them
through unchanged.

* feat(mobile): the desktop's own phone reads and writes its other-client filter

ui.get and ui.set strip the pairing-local UI fields for every paired
client. That rule exists for clients with their own host catalog (a
desktop paired to a server, a web client). A phone has none: it shows
its desktop's view, so 'Hide other-client workspaces' must be one shared
value. For the host's own phone (clientKind mobile, no relaying parent)
ui.get now always answers the field and ui.set persists it; delegated
phones, paired desktops and web clients keep the stripping rule.

The phone's view-settings schema reads the rest of the Workspace options
values it does not read yet (the automation, CLI and detached-HEAD
filters, project order, card properties, agent activity layout), each
salvaged to absent so an older desktop keeps the phone's local value.

* feat(runtime): address review of the worktree.ps row facts

Name the fact defaultBranch, and say what absence means for it: a reader
keeps its own guess, because 'hide default branch' must not start hiding
or showing rows an older host never described. Folder workspace rows get
the facts too, so every row from a newer host carries them. The
desktopOtherClient comment now says it is a verdict, not a fact. The test
covers the rows where the host disagrees with the phone's current guess.

* wip: phone uses the shared evaluator

* feat(mobile): address review of the phone's other-client filter mirror

One isHostOwnPhone predicate decides both who shares the filter value
(ui.get/ui.set) and who gets the other-client row marks, so the two
cannot drift. The pairing-local field list notes the exception. Tests
cover a paired desktop still being stripped, and a phone write that
leaves the field out not touching the host value.

* feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules

The phone's own filter chain and comparator are replaced by the shared
evaluator, reading the facts the host stamps on each row. Phone filter
state takes the desktop's setting names and carries every filter; the
badge and Clear filters match the desktop's. The desktop's other-client
marks become facts the phone filters with the shared value.

A parity test runs the same workspaces through the desktop's reading
and the phone's (facts stamped as the host stamps them) over every
filter combination, folder projects included.

* wip: phone view state carries project order and card props

* fix(mobile): host scope in shared filters, patch-key writes, live view push

Folds the phone's host filter into the shared filters so Clear and the badge agree,
writes back only the touched filter keys, and pushes workspaceViewChanged so an open
phone list re-reads desktop Workspace options.

* fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit

An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get
that started before a local edit no longer overwrites it; the edit's own change event re-reads.
The change publisher's field list now lives with it and derives the filter fields.

* feat(mobile): the phone's Workspace options sheet, rendered from the desktop's shared option list

The Filter drawer becomes the desktop's Workspace options: hosts, projects with search, group,
sort, project order, card layout and properties, card display, and every filter. Each option
renders through a table keyed on the shared option keys, so a new desktop option fails to compile
until the phone has a control. Sort and group pickers use the desktop's words; Repo reads Project.

* fix(mobile): Workspace options sheet review fixes

Hosts read as on the desktop (none checked while all show; the last shown host is disabled).
Project order falls back to the served value until the shared one is read. Card layout and card
style changes now push to an open phone. Pick-one rows read as radio buttons. Simpler wiring:
flat card state, the preset flag rides the patch, shared choices used directly.

* fix(mobile): reset project order on host switch; simpler preset-flag handling

* fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface

Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the
host resets the scope whenever a client writes the host list, so All hosts and Clear stick.
An older desktop that keeps the other-client filter to itself has its marks treated as hide.
Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the
desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values,
holds its view in one state value, and reads its UI fields from one shared list.

* feat(workspace-options): Status and PR grouping read the shared tables

The phone groups by status with the desktop's status helpers and default catalog (its own copy
had the defaults in reverse order) and by PR with a shared lane table, so its labels now read
In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same
shared state-to-lane step.

* feat(mobile): phone rows follow Card layout, Show properties, Card display and agent activity

One shared helper (getWorktreeCardDetails) decides which card details show for the chosen
properties and layout; the desktop card's flags and the phone row both read it. The phone row
hides the PR badge, note/issue/Linear glyphs, host badge and agent rows the desktop turns off,
and the Full list agent layout shows every agent row.

* test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times

* fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter

An older desktop narrows back to its legacy single-host scope when the phone
writes only a null host list, so the phone now also writes the scope for a null
or one-host list. The other-client fallback waits for the first ui.get so a new
desktop's marks don't hide rows before its filter value arrives. Inline the
clear-filters key list and note the PR state asymmetry.

* test(mobile): re-record view-settings goldens for the unknown other-client state

* fix(mobile): let a tall bottom drawer scroll on the page

The drag wrapper around the drawer's ScrollView had no flex limit, so it grew to
its content and the sheet clipped it instead of scrolling. The full Workspace
options sheet is the first drawer tall enough to hit this.

* fix(sidebar): reuse the palette's default host instead of reading the focus setting again

* fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item

* fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline

* test(mobile): give the project catalog fixture its project order

* feat(mobile): name the options chip Workspace options, as the desktop does

The chip keeps the active-filter count and reads to screen readers with the
desktop trigger's label. The pluralized filter count moves to shared code so
both surfaces word it the same way.

* fix(mobile): show the Workspace options count as a corner badge so the chip row fits

"Workspace options (2)" squeezed the Sort and Group chips to ellipses at phone
width. The count moves to a corner badge, as on the desktop trigger, and the
chip drops its icon, which the desktop trigger does not show either.

* refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back

- The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds;
  the shared evaluator and main's phone-push check both apply it, and the filter
  type derives its switch fields from it.
- git-identity.ts folds back into git-identity-display.ts, as on main.
- SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone.
- buildWorkspaceRowPredicate is module-private.

* feat(mobile): Workspace options sheet in the desktop menu's shape

The sheet showed every choice of every option, so the filters sat screens down.
It now follows the desktop menu: Hosts and Projects rows with their value, Group by
as a segmented control, Sort by, Project order, Card layout and Show properties
as rows that open a page of choices, and the filters as switches. Each option's
control (an inline row, or a value plus a page) still comes from one table keyed
by the shared option list. The segmented control is the voice settings one,
extracted so both screens share it.

* fix(mobile): Workspace options review fixes

- A page whose option stops showing is forgotten, so it cannot reopen by itself.
- Only the switch takes the tap on a filter row, so the web page writes once.
- Sections split their rows in one pass; unused heading styles go.

* fix(mobile): neutral filter switches on the page

react-native-web paints an on-switch thumb teal unless given activeThumbColor,
which React Native's types omit. MobileSwitch applies the app's switch colors,
and its web sibling also sets that prop.

* refactor(mobile): one MobileSwitch file, adding the web-only thumb color on web

* fix(mobile): Show properties reads Hover under Compact, as the desktop does

The row's value now comes from getWorkspaceCardPropertiesValue in the shared
options, which the desktop trigger uses too: Hover under the compact layout,
otherwise how many properties show. Card display shows no value, as on the desktop.

* test(mobile): find the renamed Workspace options chip in the page drawer render check

* feat(mobile): drop the Sort and Group chips; the Workspace options chip covers both

The desktop has only its options button, and the sheet now holds Sort by and
Group by. The chips' pickers, their screen state and the sort label go with them.

* feat(mobile): option pages slide in from the right and keep the sheet's height

The main sheet stays laid out under an open page, so the drawer keeps its height
going in and out. The page slides in over 200 ms and back out on back (the row,
or Android's back key), instantly under reduced motion. The reduced-motion hook
moves to platform/ now that two screens use it.

* fix(mobile): option pages fill the sheet and open at its top

- The page layer grows to the pager's height, so no part of the main sheet shows below a short page.
- The drawer takes a scrollResetKey; the sheet passes the open page, so a page opened from a row low on a scrolled sheet starts at the top.
- The main sheet's height is measured under a page too, and the pager state lives in the sheet so it resets when the drawer closes.
- A misplaced doc comment goes back to its function.

* fix(mobile): phone rows show the Jira, automation and CLI glyphs; the options pager forgets a gone page in an effect

* fix(mobile): Jira glyph sits before Linear as on the desktop; Jira reader tests beside it

* fix(mobile): the phone leaves Ports out of Show properties and Card display

* refactor(mobile): clearer names and tests for the phone's hidden card properties

* refactor(mobile): one view state and one settings read for the phone's Workspace options; tighter card glyph wiring

- The phone's Card layout and card style now live in its view state; one parallel settings.get
  reads them with pinned placement beside ui.get.
- The Card layout preset flag is a write option, no longer a view field or a change-event field.
- getWorktreeCardDetails reads the raw desktop settings and resolves the layout once; the desktop
  card computes it once in its foundation.
- The row computes its glyph presence in one place; Ports is hidden through one set.
- Drops the vanished-page effect, the native-only a11y props and the web-only switch branch.

* fix(mobile): the list settings read applies each half on its own, with tests; pinned placement changes announce themselves

* fix(mobile): Card layout writes the setting before its preset; Card display names folder paths when the desktop has folder workspaces
2026-10-15 19:27:51 -07:00
NeilandClaude 38ac75b9c1 fix(remote-terminal): draw the host's saved screen when a reattached pane's first image is blank (#29261)
* fix(remote-terminal): pull the host buffer when a pane's first attach replays no image

A remote pane that reattached to an idle long-lived agent and received an
empty (or over-limit) initial snapshot never asked the host again, so it
stayed blank with only a cursor until the agent printed something.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(remote-terminal): treat an escape-only first image as no image

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 19:25:50 -07:00
Kelvin Amoaba da27fab595 fix(native-chat): keep the "New" line in place when a turn is expanded (#29230) 2026-10-16 02:21:55 +00:00
Brennan Benson 60cf90aec4 Session History: let a scan run until it finishes instead of stopping it after 130 s (#29073)
* feat(session-history): show the scan's progress in a strip under the search box

One line under the Session History search box says what this computer's
scan is doing: "Scanning N files", "Reading n / T files", "Scanned N
files · K agents", or "Stopped after N files" with a Refresh link, which
replaces the red timeout line. Clicking it opens one row per agent. The
strip is hidden for remote hosts, All hosts and the web client.

* fix(session-history): drop the agent count from the finished strip

* fix(session-history): match an applied list to its progress by scan ID alone

* fix(session-history): let a scan run until it finishes instead of stopping it after 130 s

A scan is ended only by finishing, by Refresh replacing it, by every waiting
window leaving, or by the scanner process exiting. Stopping it after 130 s
without progress depended on every slow step reporting progress, and any
quiet one (a huge folder, a slow disk) stopped a healthy scan.

The progress counter existed only to feed that limit, so it goes too; the
strip's tally reports stay. The strip loses its 'Stopped after' state and
Refresh link, which nothing can reach now. One-off lookups keep their 15 s
bound, and older paired hosts' timeout text is still humanized.

* fix(session-history): keep Refresh available during long scans
2026-10-15 19:18:59 -07:00
Žan MlakarandNeil 69ff84db23 fix(relay): check clock skew on every cell production.tfvars declares (#28786)
* Monitor clock skew on every relay cell in production.tfvars

* test(cloud): run the clock-skew monitor workflow test in Cloud Verify

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-15 19:12:48 -07:00
NeilandYimin a06225e92b fix(terminal): focus and hover reports no longer take a mirrored terminal's size (#29239)
* fix(terminal): focus and hover reports no longer take a mirrored terminal's size

A TUI re-arms focus reporting (DECSET 1004) after every resize and xterm
answers with CSI I / CSI O. Each mirror of the terminal treated that reply as
user activity and claimed the PTY at its own grid, so two panes of different
sizes resized the PTY back and forth several times a second.

- Renderer: focus reports and button-less hover reports still reach the PTY but
  no longer claim the viewport or consume a keydown's pending interrupt intent.
- Host: terminal.send skips the viewport claim for focus reports and query
  replies, covering older clients on the stream-less send path.
- The untyped-input predicate moves to shared so both sides use one definition.

Fixes #26469
Refs #21731 (section 1: desktop-to-desktop flicker)

Co-authored-by: Yimin <47510889+Yiminnn@users.noreply.github.com>

* fix(terminal): a focus-report send with Enter or interrupt still claims the viewport

---------

Co-authored-by: Yimin <47510889+Yiminnn@users.noreply.github.com>
2026-10-15 19:11:34 -07:00
Neil 9d1cd6a7cf fix(remote): a paired client reopening a slept agent tab resumes its conversation (#29272)
* fix(remote): opening a slept agent pane from a paired client resumes its session

When a paired client opened a pane whose PTY was gone, the host started a
bare shell (or a fresh agent) instead of resuming the agent session it had
recorded for that pane. Resume from the pane-owned record the way the host
window's cold restore does.

* fix(remote): label a resumed pane with the agent it resumes

* fix(remote): do not resume a conversation another pane also records

* fix(remote): send a pane resume through restore admission

* fix(remote): do not resume a conversation already live in another pane

* fix(remote): block a pane resume while another holder is not proven exited

* fix(remote): do not resume a record the pane's hook identity has superseded

* fix(remote): an unhydrated session holder blocks a pane resume

* fix(remote): compare full provider-session identity in the resume guards
2026-10-15 19:10:56 -07:00
Neil dcdf3860e1 ci: share runtime test imports and Node 18 preparation (#29300)
* ci: share runtime test imports and Node 18 preparation

* ci: run native plugin reconciliation on Node
2026-10-15 19:05:26 -07:00
Brennan Benson 18fcbe5a15 fix(claude): a slash command Claude answers before echoing opens its send's turn (#29013)
Claude answers a local command such as /goal before it echoes the send, so
that first reply opened a turn of the provider's own and the send's bubble got
a second, separate Worked-for bar. The CLI already says which send it started
(command_lifecycle started), so a turn opened by output while that send is
still unechoed now names the send as its opener.
2026-10-15 19:04:21 -07:00
Kelvin Amoaba 1f6c6e5483 fix(cli): publish a readable /usr/local/bin/orca symlink on macOS and repair unreadable ones (#29245) 2026-10-16 02:00:02 +00:00
Brennan Benson 295202720a fix(daemon): adopt the v47 terminal daemon the 1.4.227 release runs (#29191)
* fix(daemon): adopt the v47 terminal daemon the 1.4.227 release runs

1.4.227 is built from 1.4.224 plus picks and takes daemon protocol v47 so
it can adopt the v45 daemons 1.4.225/1.4.226 left running. Main skips to
v48 and lists 46 and 47 as previous, so updating from 1.4.227 to the next
main release keeps every live terminal.

* fix(daemon): the build compatibility contract names daemon v48

* fix(daemon): the TS copy of the compatibility contract names daemon v48

* test(daemon): the macOS launchd label test expects daemon v48
2026-10-15 18:58:45 -07:00
Kelvin Amoaba 6184d0ea28 fix(browser): always allow leaving when a page asks to confirm (#29142) 2026-10-16 01:55:55 +00:00
Neil 57125285fc Keep collapsed sidebar agent cards steady when the child arrow appears (#29246)
* fix(sidebar): keep agent card height stable across child disclosure

* test(sidebar): update child row snapshots for stable disclosure height
2026-10-15 18:55:18 -07:00
Neil 9be9d1119f fix(syntax): give a cold grammar enough attempts to compile (#29290)
A grammar's first line also compiles its regexes, which TextMate cannot
interrupt. On a slow or busy core that compile alone could outlast all
three 50ms attempts, marking the block degraded and leaving it plain for
good. Allow a grammar that has never finished a line up to ten attempts;
warm grammars keep three. Also reverts the ineffective test-timeout bump.
2026-10-15 18:42:32 -07:00
NeilandClaude 97469d64bd fix(session-tabs): rebuild an emptied headless session snapshot instead of hiding saved terminals (#29283)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 18:41:55 -07:00
Neil 69fcd672d8 fix(profile-state): keep the SQLite failure behind a startup open error across the writer worker (#29280) 2026-10-15 18:40:21 -07:00
Brennan BensonandOrcaWin 7c23f79622 fix(quick-commands): save agent prompts whole, bounded by what a chat message carries (#24488)
* fix(quick-commands): save agent prompts whole, bounded by what a chat message carries

Agent quick-command prompts were silently cut to 6,000 characters on save:
the editor accepted a long paste, settings normalization trimmed it, and
reopening showed the shortened copy. The 6,000 was a Windows launch speed
threshold, not a limit on prompts.

- Storage no longer trims. A save is refused, never trimmed, when a prompt's
  chat text block is over 256 KiB of JSON (measured on its real escaped size,
  the measure the chat send schema applies) or the list is too large for one
  paired reply. Host and desktop store both check before writing.
- The editor shows the size against the limit and blocks Save past it, and
  stays open with the text when the host refuses the save.
- terminal.quick-commands.long-prompts.v1: hosts withhold prompts over 6,000
  characters from clients that do not advertise it (older clients reject a
  whole list holding one); editors keep an older host's 6,000 cap.
- The phone editor no longer drops pasted text past 6,000 via maxLength.

Launch delivery of long prompts is left to the launch-prompt series
(#24257, #23962, #24080); this branch's earlier paste-after-ready delivery
duplicated it and is removed.

* fix(quick-commands): drop a stale duplicate doc comment

* fix(quick-commands): advertise long prompts to this build's own host too

* fix(quick-commands): keep submitted editor immutable until save settles

* test(quick-commands): keep storage and save lifecycle coverage focused

* fix(quick-commands): keep portalled choices within pending form ownership

* Preserve retained credentials when settings validation refuses a save

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-15 18:37:41 -07:00
Brennan Benson d4e53c6107 Fix keyboard focus when revealing a chat in another pane (#28987)
* Transfer keyboard focus when a sibling chat pane opens

* fix(native-chat): hand focus to newly opened chat composer

* fix(native-chat): keep tab bar controller within line limit

* fix(native-chat): start focus expiry after tab opens
2026-10-15 18:32:54 -07:00
Brennan Benson fb0994944f fix(native-chat): link a bare file name once the chat's folder has it (#28818)
* fix(native-chat): underline a bare file name once the chat's folder has it

An agent's reply naming a file it created in its own folder, e.g.
`decision-explainer.html`, stayed plain inline code: chat required a
slash before it would even ask the host whether the path exists. Every
chat agent runs in one known folder (the host's launch directory, which
chat already resolves its links against), so a bare name the terminal's
bare-word detector accepts now goes through the same host existence
check and links only when the file is there. Version-like names (v1.2)
and spaced names are still never asked about.

Fixes STA-10392

* test(native-chat): drop an href assertion the click test already covers

* Link whole inline-code filenames in native chat

* fix(native-chat): keep version strings in inline code plain

Bare names with a dot now pass chat's existing inline-code shape rule, so v1.2.3 and 1.0 stay plain as on main.

* refactor(native-chat): name the inline-code file rule for what it checks
2026-10-15 18:31:07 -07:00
Brennan Benson e9c77360f5 fix(mobile): open each chat at its newest message when switching chats (#29099)
* fix(mobile): open each chat at its newest message on a chat switch

The chat view stays mounted across chat tabs, so a reader's scroll in one chat
carried into the next: it opened mid-transcript and not following. Reset follow
intent and the page's history head when the chat changes, before paint, and keep
view-wide state (Tools toggle, text size).

* fix(mobile): pin a switched-to chat to its own end on the page

The page pin scrolled to the last reported content height, which after a
chat switch is still the previous chat's, so a taller chat could paint
one frame mid-history before its own height arrived. Pin to the scroller's
live height instead.
2026-10-15 18:29:26 -07:00
865cdaa706 fix(native-chat): acting on one chat no longer waits on checks of every other chat (#26555)
* fix(native-chat): acting on one chat no longer waits on checks of every other chat

Starting, resuming or switching a chat's agent first ran restart
reconciliation, which probed every unreconciled session and committed them
together, so one slow or failing probe for another chat delayed or failed
this chat's action. Acquisition now reconciles only the addressed session;
the explicit startup pass still covers all of them, and an owner that isn't
proven still can't be granted a writer.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native-chat): settle each restored chat on its own; register new sqlite tests

- Startup restore no longer lets one chat's failed lease check skip recovery for the others.
- Run the two new SQLite-backed tests in the Node runtime project.
- Startup reconcile runs the whole-host pass without a placeholder session id.
- Test names describe an injected failing/stalling owner check, not an SSH host.

* fix(native-chat): restore keeps one whole-host lease check per startup

The restore opens with the whole-host pass again and stops per-chat bookkeeping after the
first failure, as on main: on a failing store it makes one lease-check write, not one per
chat. User actions stay targeted to their own chat.

---------

Co-authored-by: OpenAI Codex gpt-6.1-sol <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:28:43 -07:00
Neil a622f9efd3 fix(mobile): stop "Opening workspace" spinning forever when the page never loads (#29274)
* fix(mobile): fail a shell document the view never finishes loading instead of spinning forever

A load the WebView refuses or never commits reports nothing, which is how iOS 18
left the OTA workspace page on "Opening workspace" forever (#28626). Arm a
document-load deadline whenever the view is given a document; expiry takes the
existing document-load-failed recovery (drop the cache, refetch once, then the
failure screen with Retry).

* refactor(mobile): arm both shell document deadlines from one case
2026-10-15 18:27:25 -07:00
699709c0ba fix(ssh): never leave an SSH host unconnectable after a refused move (phone users, #29083, Move anyway) (#29220)
* fix(ssh): never leave an SSH host unconnectable after a refused move to its managed server

* fix(ssh): keep unsaved drafts out of what Move anyway leaves behind

* test: run the stuck-host profile test in the Node runtime project

* fix(ssh): spend a "Move anyway" consent when the conversion throws

A throw left managedServerLeaveBehind set, so a later connect left saved
state behind without asking again. A resumed fence replays its journal,
never the export, so clearing is safe.

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "fix(ssh): spend a "Move anyway" consent when the conversion throws"

This reverts commit 4f63d777d9.

* docs(ssh): note why a Move anyway consent survives a conversion throw

* fix(ssh): Move anyway needs every terminal proven exited, and skips foreign tabs fully

- A bridged terminal proof (relay census live or unverifiable) now refuses a
  confirmed "Move anyway" before any fence or journal, with a reason that says
  to close the terminals; the host stays on its relay and connectable. The
  offer itself is withheld while terminals aren't proven exited.
- A foreign tab left behind is also removed from the legacy tabsByWorktree
  rows, its layout, active-tab pointer, relay session id, scrollback, and
  pane-keyed resume records, so it can't reappear on the managed server.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): Move anyway drops left-behind files' tabs, and is offered only when it would work

- An editor tab whose host-bound file is left behind is removed with the
  file, so the managed server never opens it as a blank pane.
- The offer is previewed against the leave-behind export first: when what
  refuses is a desktop-only draft the export can't leave, no "Move anyway"
  is shown and the save-or-discard reason stays.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): drop surface and closed-tab tombstones of foreign tabs Move anyway skips

* fix(ssh): match legacy pane keys when Move anyway drops a foreign tab's records

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 18:26:18 -07:00
Neil 66631d0d26 perf(ssh): avoid idle wait after Windows pipe probes (#28560) 2026-10-15 18:20:48 -07:00
NeilandClaude 99064a5e11 fix(worktree): a worktree created from a remote branch tracks it (#26344) (#29275)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 18:19:03 -07:00
Brennan Benson d6f9df9b74 fix(native-chat): settle acknowledged messages after a paged reconnect (#28993)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation
2026-10-15 18:16:56 -07:00
Brennan Benson 131986aa16 feat(worktree): show checkout progress while a worktree is created (#24093)
* feat(worktree): show checkout progress while a worktree is created

A local create spends most of its time in the checkout `git worktree add`
performs, and the card only said "Creating worktree…" for that whole time.
Git already writes its "Updating files: N%" meter into our stderr pipe, so
the add call reads it passively (same command line and environment) and the
create card shows "Checking out files… N%" with a thin bar. All six create
labels are translated so a create never switches language mid-flight.

Rebuilt on main from the version stacked on the create-gating branch. On
main a create can also claim a prepared checkout; that path never runs
`git worktree add`, so it keeps today's "Creating worktree…" label with no
bar, and a missed claim falls back to the plain add, which shows the bar.

* test(worktree): pin that finalizing a prepared checkout reports no checkout progress

Claiming a prepared checkout never runs `git worktree add`, so the create
card keeps its label with no bar; this pins that at the finalize step,
including the retarget `reset --hard`.

* test(worktree): keep only the checkout-progress tests that pin a contract

Drops unit tests that mirror the implementation; the real-git flow, the parser
captures, the cross-version Git contract and the create card test remain.

* test(worktree): pin the checkout meter's path from git to the create card

* test(worktree): merge the narrow-terminal capture's pipe chunks
2026-10-15 18:14:56 -07:00
Brennan Benson 48d080e971 feat(acp): a reclamation primitive removes all but the kept managed runtime release (#28507)
* feat(acp): ACP agents can run with a private per-process temp folder

* feat(acp): a reclamation primitive removes all but the kept managed runtime release

The installer calls it while it owns the runtime's installs and launches; it answers the entries it
could not remove so that caller can report them. Nothing calls it from adapter construction.
2026-10-15 18:13:57 -07:00
OrcaWinandOrcaWin fb529aaa30 test: batch lint fixtures and remove repeated runtime scan (#29286)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 18:13:10 -07:00
Brennan Benson f06404d7a8 feat(acp): ACP agents can run with a private per-process temp folder (#28462) 2026-10-15 18:13:03 -07:00
OrcaWinandOrcaWin 01800a092a chore: update Oxlint and Oxfmt to latest releases (#29291)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-15 18:11:48 -07:00
Neil ac015cd3b2 Fix terminal daemon restart leaving older Codex conversations locked (#29216)
* Recover retired macOS daemon trees during terminal restart

* fix(daemon): never let retired-job recovery failure block restart

A wedged retired job (unkillable descendant, launchctl timeout, recycled
root) previously aborted every untargeted Restart Terminal Daemon, leaving
no way to replace the current daemon short of a reboot. Log the failure
and continue with the ordinary restart instead.
2026-10-15 18:08:22 -07:00
NeilandClaude Opus 5.5 9fa50a3d6d fix(native-chat): paste clipboard images into a chat on a managed SSH host or paired server (#29278)
* fix(native-chat): paste clipboard images into a chat on a managed SSH host or paired server

Since SSH hosts moved onto managed Orca servers, a terminal-backed chat on one resolves
to a remote owner, which refused every clipboard image. Save the image on that server
(its own host runs the agent), as a terminal paste already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native-chat): let a server-saved clipboard image through the composer's remote-target guard

The composer refuses client files on a remote PTY when it starts the pending chip.
An image the paste saved on that server is not a client file, so mark the chip
server-owned, including when it settles as a file reference. Cover the flow with
the real attachment hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(native-chat): drop an unused paste option type widening

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:06:42 -07:00