Skip to content

sync: upstream ec5eaf96c1 into swaplabs/main - #38

Merged
kiwidream merged 3486 commits into
swaplabs/mainfrom
sync/upstream-20261022
Oct 22, 2026
Merged

kiwidream merged 3486 commits into
swaplabs/mainfrom
sync/upstream-20261022

Conversation

@kiwidream

@kiwidream kiwidream commented Oct 22, 2026 •

Copy link
Copy Markdown
Files Added Deleted Net
Test 1084 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​94815 $\color{#cf222e}{\Huge{\mathbf{−}}}$​43987 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​50828
Prod 1916 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​236136 $\color{#cf222e}{\Huge{\mathbf{−}}}$​99277 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​136859

ELI5

Upstream Orca moved forward 3,484 commits in the three weeks since the last sync. This PR merges them into swaplabs/main. It also rebuilds every fork feature that collided with upstream's changes on upstream's new code. 94 files conflicted; each is resolved below, grouped by the fork feature it belongs to.

What Changed

Upstream range 5f728f7862..ec5eaf96c1, 3,484 commits (compare). One signed merge commit, 1a247d1c5c.

Upstream restructured much of the code the fork hooks into:

  • src/relay moved to src/wsl-guest, and the SSH relay is gone;
  • src/shared/child-process became the @orca/process-host package;
  • sidebar grouping moved to src/shared/sidebar-list;
  • runtime construction was split up;
  • 14 files the fork had edited were deleted.

Every fork feature below keeps its behaviour unless it says otherwise. Fork logic that no longer fit an upstream file's 300-line budget moved into fork-owned modules.

Conflicts by fork feature

Fork feature Resolution
Config, CI, docs electron.vite.config.ts: the fork's update-source defines moved to config/build-plugins/swaplabs-update-defines.ts and are spread into both define blocks. pr.yml: the sync-PR diff base is kept on upstream's rewritten gate steps. AGENTS.md: the fork section is appended to upstream's rewrite, plus a pointer to the new resume doc. Also .gitignore, build-constants.d.ts, and locales (union of keys; upstream's new pt.json gets the fork's title string). Two test inventories upstream deleted (stablyai#25791) are taken as deleted.
Update sources (#8 #10 #13 #14 #15 #16 #17 #27) Ported onto upstream's updater rework (handler context, mac staging, SQLite-downgrade guard, install-retry UI). Refusal builders and the feed fixture moved to new modules for max-lines. The #17 "How to update" link moved into upstream's new server row details. mac-self-update and the swaplabs-* config scripts now use @orca/process-host. The fork's startup-ordering assertion moved to mac-self-update-launch-wiring.test.ts, because upstream deleted the file it lived in.
Daemon drain (#25) Drain routing moved to daemon-router-drained-daemon.ts, and the endpoint hand-over seam to daemon-endpoint-handover.ts, both for max-lines. Upstream's orcad cache cleanup now recognises drain slots.
Worker release (#34) The closing-turn wait is typed per release mode. The isStructuredWorkerHandle import moved to shared code; without that fix, every interactive release throws.
Unreachable-host removal (#3) Ported onto upstream's new removal flow through project-removal-owner.ts.
Delegated worker nesting (#4) Moved into upstream's shared row plan (sidebar-list/delegated-worktree-nesting.ts, a 6-line seam in sidebar-row-plan.ts). The three deleted grouping files are removed.
Resume (#1 #5 #6 #9 #11) #1's retention is ported onto upstream's shared resume-record rule (stablyai#30455); the rule itself did not change. #6's scan coverage is carried through every scan path. The fork's SSH relay scanner goes with upstream's.
Terminal-loss resume (#30) The hook store keeps the identity on terminal loss, ported onto upstream's restructured server and src/wsl-guest. The host resume is wired as a fallback inside upstream's restored-pane start (see decisions). agent-status-store.md (deleted upstream) gives way to docs/reference/swaplabs-agent-resume.md.

Decisions made at the operator's request

The operator asked for these; under the sync rules they would have stopped the run.

Why

The daily sync automation didn't run between 2026-10-03 and 2026-10-21, a known issue that is now fixed. This sync catches up.

Linked Issue

N/A: daily upstream sync. Follow-ups are linked under Notes.

Visual Proof

N/A. UI touched only by ports: the update settings rows (#14), the remove-folder dialog (#3) and the resume chooser (#6). Each component's tests pass.

Testing

  • Typecheck: run-typecheck-projects-in-parallel.mjs, all four projects: "No type errors" (node 17,549 files, web 15,637, cli 3,109, process-host 34).

  • Changed-code gate: check-changed-code-quality.mjs with ORCA_SYNC_PR_DIFF_BASE=ec5eaf96c1 passes with 0 findings across 329 fork files. check-react-doctor-changed.mjs on the commit passes.

  • Lint and format: oxlint and oxlint --config config/oxlint-react-doctor.json pass over all 22,121 staged code files. oxfmt --check is clean on every fork file.

  • Fork tests: every test file the fork adds or changes, 116 files: 1,464 passed, 8 skipped.

  • Wide run: every test under the 55 directories the fork touches, 10,798 files: 10,780 passed, 18 failed. Re-running the 18 with the repo's bun on PATH leaves 10, all environmental on this host:

    • real-bash 5.3.9 prompt tests (4 files);
    • no /bin/zsh;
    • no pg in cloud/;
    • node-gyp;
    • a Windows benchmark;
    • a Bun API gap;
    • stale cross-version checkouts.

    None of them is a test the fork changed.

  • Pre-commit hook: skipped for the merge commit. Its oxfmt --write would have reformatted docs/automated-setup-tab-design.md, an upstream file the merge carries unchanged. Its two oxlint passes were run by hand over every staged file (above).

AI Disclosure

Prepared by the "Orca upstream sync" automation (Claude Code, Claude Opus 5.5) with per-feature resolution agents. The #30, #4 and #25 decisions were made at the operator's request in the same session.

Agent skill upstream boundary

  • Not applicable: this PR merges upstream's history and ports fork features; it adds no agent skill.

Notes

orca-fork-ops status for swaplabs/main (9e364c6cd4) against upstream ec5eaf96c1, before this merge:

  • merged upstream: 0;
  • pending upstream: 0;
  • local forever: 80;
  • 30 commits have no LOCAL: prefix and no upstream PR.

The 17 missing upstream release tags (v1.4.219 to v1.4.237) were copied to the fork. This PR supersedes #37, whose head is main.

Follow-ups are filed as issues:

🤖 Generated with Claude Code

nwparker and others added 30 commits October 21, 2026 00:11
…lyai#31447)

The runtime WebSocket listener widens to 0.0.0.0, which never accepts
IPv6, yet the Connection address picker and the default pairing address
offered global IPv6 interfaces, minting links nothing answers. Filter the
interface list by the family of the host the listener binds when serving
the network.

Reported-by: @fongyuan-2011
…names the browser routing setting too (stablyai#31444)

* fix(ssh): the host's 'no desktop is sharing its browser' error names the browser routing setting too

The desktop withholds the browser bridge from a managed SSH host whose pages browse from the
desktop's own network, but the host's CLI was told only to turn on the per-host CLI control
switch, even when it already was on (stablyai#31421).

* fix(ssh): name the per-host routing opt-out too, and guard the labels against UI copy drift
…lyai#31433)

Let the finger that creates a long-press selection extend it through the existing selection-drag dispatcher. Verified with native iPhone touches and focused lifecycle tests.

Fixes stablyai#30554
Adapted from community PR stablyai#30555.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…i#31432)

Retire old terminal stream callbacks and delayed fits across reconnects, unsubscribe, terminal replacement, and synchronous termination. Preserve fresh output and the latest accepted fit.

Fixes stablyai#30505
Adapted from community PR stablyai#30506.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…tablyai#31203)

* fix(native-chat): typing fast after focus moves keeps keys in order

Typing a burst right after focus left the composer (Move Tab to Split, a
paired chat, a click on the transcript) could scramble the first letters:
"Reply exactly" came out as "Rply exactly...e".

The first key was routed into the composer programmatically: focus moved
at once, but the caret was set a frame later from a position computed
before the key. The next keys landed natively, then that late caret write
pulled the caret back. Quote "Add to chat" and picker completion had the
same deferred caret write.

Each programmatic write now puts the text and caret into the editor in one
synchronous step, read from the editor's live value, then focuses it. No
write happens while an IME composition owns the field.

* fix(native-chat): retain saved chips after synchronous composer edits
stablyai#31453)

* fix(codex): copy from a paired client's or WSL Codex reaches the viewer's clipboard

* test: type the Claude launch fixture
…yai#31450)

* Measure Windows ARM qualification worker concurrency

* Reuse Windows server prebuilds across app version bumps

* Match native cache measurement to adhoc compiler settings

* Keep measured cache fix and remove temporary comparison modes
…ai#31434)

Fixes stablyai#30301

Supersedes community PR stablyai#30325. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…i#31435)

Fixes stablyai#27498

Supersedes community PR stablyai#30297. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Fixes stablyai#30302

Supersedes community PR stablyai#30314. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…ai#31437)

Fixes stablyai#30300

Supersedes community PR stablyai#30312. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…tablyai#31442)

Fixes stablyai#16215

Supersedes community PR stablyai#16216. Original contributor credit is preserved below.

Co-authored-by: iBakuman <67883044+iBakuman@users.noreply.github.com>
Fixes stablyai#30517

Supersedes community PR stablyai#30518. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…ai#31445)

Fixes stablyai#25457
Fixes stablyai#25458

Supersedes community PR stablyai#25432, community PR stablyai#25435. Original contributor credit is preserved below.

Co-authored-by: Prateek Rungta <prungta2@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ay (stablyai#31446)

Fixes stablyai#17168

Supersedes community PR stablyai#17220. Original contributor credit is preserved below.

Co-authored-by: Arthur Chang <arthurc0102@gmail.com>
Fixes stablyai#14321

Supersedes community PR stablyai#14787. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
…le on headless hosts (stablyai#31460)

On a headless host the mobile status builder treated any title without an
agent marker as the shell reclaiming the pane. Codex's idle title is its
project name, and since OSC 7501 program status gives the pane a PTY state,
a Codex turn with no tool call ended with the hook's transcript address
withheld, so the phone showed "Can't find this conversation yet".

Only a shell or management title now retires the hook identity, matching the
renderer projection. Refs stablyai#31136.
…yai#31441)

Fixes stablyai#18283

Supersedes community PR stablyai#18347. Applies the existing desktop WebGL patch to the pinned mobile engine after independent adversarial and upstream review.

Co-authored-by: fettpl <38704082+fettpl@users.noreply.github.com>
)

* fix(i18n): correct Korean sleep with descendants wording

* fix(i18n): keep Korean descendant sleep label compact

---------

Co-authored-by: Jaeyoung22 <89302528+Jaeyoung22@users.noreply.github.com>
…blyai#31467)

Adopt the nine Japanese wording corrections from community PR stablyai#29248. Clarify smartphone control, manual restoration, and mobile data connections.

Closes stablyai#29247.

Co-authored-by: sojiro-o <53679703+sojiro-o@users.noreply.github.com>
…ort it connected (dark, layout-owner build only) (stablyai#31485)

* fix(layout): a running agent's resume record is not a sleep in switch-on builds

The shared layout load put every pane resume record into leaf.sleeping, so main's
terminal index read a live or quit capture as a slept pane and terminal.list
reported running agents as disconnected and unverifiable. Records of slept panes
(isPaneSleepRecord) now load to leaf.sleeping and running agents' records to
leaf.resume; both save to the same sleepingAgentSessionsByPaneKey entry, so the
disk format is unchanged. The disk-field census names both holders, and a model
rule refuses a pane holding both.

* fix(layout): a waking start wakes only slept panes; a recordOnly wake drops either record

* fix(layout): main's shutdown reconnect facts read a running agent's resume record as awake
…nnect can't image its pane (stablyai#31482)

* fix(remote-terminal): repaint an idle program when a reconnect can't image its pane

After an SSH reconnect the pane is repainted from the host's screen
snapshot. When the host cannot give one (too large for the replay limit,
no buffer, unanswered, overflowed), the pane only printed a loss banner and
an idle full-screen program that writes diffs kept its pre-disconnect frame
until the user resized.

The two places a remote pane declares it has no host image now ask the
program to repaint through one choke point: a one-row viewport claim and
back, which every TUI honours (Node's tty ignores a same-size SIGWINCH).
It respects a phone or another desktop holding the size, skips hidden
panes, and puts the row back even if the pane unmounts mid-nudge.

Fixes stablyai#26887

* test(e2e): idle full-screen program frame after a managed SSH outage

* test(e2e): wait for the pane before typing in the idle-frame spec

* fix(remote-terminal): pick the redraw height on the host's clamped grid

* fix(remote-terminal): wait for the host's grid before putting the redraw row back

* test: give transport mocks a connection status for the redraw gate

* test(e2e): keep the orcad idle-frame proxy typed for the e2e ratchet

* fix(remote-terminal): let a new PTY's redraw supersede a swapped-out nudge

* test: keep new transport-mock lines free of type assertions

* ci(e2e): route the managed-SSH idle-frame spec to its sources
… a paired client (stablyai#31461)

* feat(accounts): add Claude and Codex accounts to a remote server from a paired client

The server runs the login (Claude's pasted-code sign-in, Codex's device
code) and keeps the credentials; the client only opens the page and
relays the code. New capability-negotiated accounts.hostSignIn* methods;
older servers degrade to an update message.

Fixes stablyai#20009

* fix(accounts): settle host sign-ins by the login's outcome, pin pairing, guard stdin

Review fixes: a cancel the login refused still reports the added account;
the client pins each flow to its pairing revision; Claude's stdin EPIPE no
longer crashes the host; both sides accept only the provider's sign-in
host; pasted codes reject whitespace; a host-side supersede says so; Codex
cancels its own login handle; opt-in real-CLI test.

* fix(accounts): keep an early Cancel, retry polls through the host's window, scan Claude stdout only

A Cancel clicked before the host answers the start is applied once it does;
the client keeps polling through a short outage until the host would have
ended the flow; Claude's link is read from stdout only; a timeout after the
code was sent keeps its own message.

* test(accounts): name the stdin sender and allow its stream write in the persistence ratchet

* fix(accounts): register each Add Account attempt before its first await

One attempt per click owns the pairing pin, the cancel flag and the host's
flow, so a Cancel during the capability check or the start is never lost;
polls retry only lost answers; the stdout forwarder is removed on cleanup.

* refactor(accounts): drive the client sign-in as one explicit state machine per server and provider

checking -> starting -> following -> settled, with Cancel as a flag the run
loop delivers once the host names its flow and re-asks after any answer
until the host takes it. A second Add joins the running attempt; a Cancel
the server could not be told about is surfaced; retry policy tested.

* fix(accounts): retry a lost start answer, and report a cancelled sign-in as cancelled however it ends

* fix(accounts): Cancel reaches the server a sign-in runs on, and a typed code stays with its flow

The host picker can move while a sign-in runs: Cancel now targets the
running server sign-in (or the desktop's own login when none runs), and
the pasted-code form is keyed by server and flow.

* fix(accounts): run at most one server sign-in per provider, so Cancel has one meaning

* fix(accounts): the server sign-in prompt owns its Cancel

Cancel for a server sign-in now lives in the prompt bound to that server,
which also reappears with its Cancel when Settings reopens mid-sign-in; the
header Cancel is only for this desktop's own login.

* fix(accounts): hide the desktop-login Cancel while a server sign-in runs; narrow the prompt's status

* fix(accounts): keep the desktop-login Cancel in local scope even while a server sign-in runs

* fix(accounts): the header Cancel follows the running addition, wherever Settings now points

Each Add Account records where it is signing in. Cancel stops that
sign-in, on a server or this desktop, and is hidden only where that
server's own prompt already offers Cancel.
…tablyai#31462)

* refactor(renderer): read workspace records through typed selectors

* refactor(renderer): keep only the selectors with readers; type the host owner lookup
… platform (stablyai#31508)

A WSL distro is always a POSIX host and its deploy already detected Linux, so
the bridge skips the second uname round trip (100-150 ms per connect). SSH hosts
still probe exactly as before.

Co-authored-by: Claude <noreply@anthropic.com>
… size (stablyai#31495)

* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character

129 non-ASCII characters in regex literals (and three in a String.raw plugin
template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte
string. Write them as \uXXXX escapes and fail the build if any return.

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

* fix(orcad): keep license comments ASCII and name the module the guard trips on

esbuild gathers legal comments at the end of the bundle; escape that comment so
a dependency's license text cannot fail the ASCII guard, while licenses still
ship with the code. The guard now reports the offset and, from an unminified
rebuild, the module the character came from. A dark-switch build is covered.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…d page (dark, layout-owner build only) (stablyai#31509)

* fix(browser): a paired desktop shows a server's browser tab without opening a second page (switch-on builds only)

With the layout-owner switch on, a server browser tab drawn from the server's layout
reaches the desktop without a saved page handle. Showing it made the desktop create a
new page on the server, so the server went from one page to two.

Before creating, the desktop now asks the server for its browser pages and adopts the
one already named by the tab's page or browser tab id. It creates a page only when the
server has none, when the lookup fails, or when the server gives no list. A pane that
moved on during the lookup adopts nothing. Switch-off builds keep creating directly.

* test(browser): explain the fake runtime call's cast in the page session test

* fix(browser): adopt a server's browser page by the id its layout already names, not by listing pages (switch-on only)

This replaces the tab-list lookup. A tab list misses saved pages a restarted server has
not started, wakes the active saved page as a side effect, and can match a page another
client hosts.

The server's layout already records the page id on the browser tab. Desktop hydration
ignored it whenever the page record had not landed yet, and made up a new id on every
frame build. With the switch on, hydration now uses the tab's recorded page id, so the
desktop row is named by the server's page from the first frame and stays named by it.

A frame-drawn row still has no saved page handle: hydration has none for server-hosted
pages, and the server environment is known only to the pane. So, with the switch on, the
pane probes its row id with browser.tabShow. tabShow also starts a saved page the server
has not opened since a restart. On success the pane stores the handle and streams that
page. On not-found, or any other error, it creates a page as before and closes nothing.
A pane that moved on during the probe adopts nothing.

Why not reorder the server to save the page record before opening the tab: it would only
fix the id, not the missing handle, so the probe is needed either way. The page record
write also depends on the tab main writes asynchronously, so the order cannot be made
certain from the server alone.

* test(browser): explain the lifecycle harness's fake runtime call cast

* fix(browser): create a server browser page only when the server lacks it, and prefer the tab's active page id

The switch-on probe created a page after any tabShow failure, so a timeout or a brief
connection error could duplicate a page the server still runs. Now only a page-missing
answer creates one. Any other error is raised, as the stored-handle path already does, and
the pane shows its stopped state with Reconnect.

Hydration now names a tab's page by its recorded active page before its first page id,
which matches the page the server starts for a saved tab.
…rs (stablyai#28952)

* 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(chat): use terminal pane opacity for split chat focus

* test(chat): verify split opacity follows the focused group

* fix(chat): an unfocused split chat dims exactly as much as a split terminal

A split chat was dimmed with the terminal's inner-pane opacity (0.9 by
default) while terminals and editors in an unfocused split group get the
group's 0.95, so chats looked dimmer than terminals beside them. The chat
layer now takes the same shared group dim and nothing more.

* fix(chat): terminals and chats in an unfocused split dim like editors

Editors and tab bars in an unfocused split group were dimmed to 0.95, but
terminals and chats are drawn in a layer outside the group, so they stayed
at full opacity. One shared opacity now dims the group panel and the
retained terminal and chat hosts alike, so all three look the same.
…ge (dark, layout-owner build only) (stablyai#31529)

* fix(session-tabs): a windowless layout-owner server lists and pushes main's tab groups and order

With the layout-owner switch on and no window (orca serve, orcad), session.tabs rows came from
the snapshot hydrated from the saved session; main's layout was merged in only when a window
published. A routed reorder or Move to Split reached main's layout but not session.tabs.list or
the phone. Main now merges its layout into the rows it holds on each layout change and before
each list, with the same merge a window publication gets.

* refactor(session-tabs): keep the windowless merge with main's publisher, under the file's line cap

* WIP

* fix(session-tabs): every stored tab row follows main's layout, with or without a window

With the layout-owner switch on, the one writer of the rows phones read now merges main's
layout into every write for a workspace main owns, and main re-merges the held rows after
each layout commit on any host. A CLI rename, create or close reaches phones without a
window publish, and a row main's layout dropped cannot come back from an older snapshot.
A write that main's layout wholly overrides stores nothing, so list calls on settled rows
bump no version and push nothing. A pane's own title now counts as a manual name.

This commit also untracks a local vitest config and a scratch test that the previous
work-in-progress commit picked up by mistake.

* test(session-tabs): a rebuild from the saved session and a CLI rename keep main's groups and names on phones

* fix(session-tabs): the row writer only reorders and regroups; layout commits add main's new rows

With the layout-owner switch on, the one writer of the rows phones read now takes only main's
tab order and groups. It never adds or drops a row and never replaces a terminal id, so a
freshly started terminal keeps its id and rows the exit filters removed stay removed.

After each layout commit, on any host, the held rows take main's new rows, names and order,
keep any row main's layout lacks (closes are the close path's job), and pass any added
terminal through the same exit filters the window path uses. A main binding that runs nowhere
no longer replaces a running terminal's id. Both read main's last projection, so neither
adds a session read per write or per workspace.

A CLI close still leaves its row until the dropped-leaf hook lands; its test is marked as an
expected failure until then.

* test(session-tabs): the row writer's order and a commit merge's keep rules, unit by unit

* fix(session-tabs): a kept row keeps its group, and an unchanged commit pushes nothing

A layout commit's merge placed a row it kept (a tab main closed, until the dropped-leaf hook
removes it) by its row id, but groups list a terminal by its tab, so the row fell into the
active group. Both the writer's reorder and the merge now place rows through one shared
group-placement step keyed by the tab.

A commit now pushes only when the writer stored something. The writer skips a write only
when it keeps the stored occupant, publication and retirement proofs. A terminal row a commit
rebinds also passes the exit filters, a main binding the host saw exit no longer replaces a
running terminal, and a binding main made before its terminal connected reaches the rows when
it connects.

* test(session-tabs): a kept row's place and the no-push commit are separate tests

* fix(session-tabs): a rebuild with a new publication stamp is still skipped; simpler commit merge

The writer's skip compared publication stamps, but a rebuild from the saved session mints a
new stamp from the clock each time, so every rebuild minted a version and pushed. It now
stores only for a different workspace instance or new retirement proofs; an absent one keeps
what is stored.

The commit merge again runs the exit filters only for added terminal rows (they admit any row
the held rows list, so a rebind changed nothing), and a terminal connecting now schedules the
merge itself rather than a separate check, since an unchanged merge stores and pushes nothing.

* refactor(session-tabs): a commit merge pushes after every store again; name the window-path exit test

No reachable case was found where a commit merge's rows differ from the held rows while the
writer keeps the stored ones, so the extra "only when stored" check had nothing to guard and
goes. The exit-rule test now says it covers the window's publication path too.

* fix(session-tabs): a window reload's new publication stamp is stored, not skipped

The writer skipped a write that main's order wholly undid without comparing publication stamps,
so a window reload that republished the same rows under a new stamp kept the old one. The
reconciler then saw its accepted stamp missing and asked the window to republish on every sync,
and a host-built stamp could stay on window rows. The skip now treats stamps as unchanged only
when both are host-built, so repeated rebuilds from the saved session still mint nothing.

The windowed test of a row main's layout lacks also replays the window's older snapshot.
Jinwoo-H and others added 14 commits October 22, 2026 06:37
…n from the server's layout (switch-on builds only) (stablyai#31853)

* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* fix(L273-core): a simulator tab opened in a paired server's frame-drawn workspace survives its frames (switch-on)

The server never has this desktop's emulator tab, so its frame rebuilds dropped it. The simulator
open now records the tab as created here when a server's frames draw the workspace, and opens it
in the server group it would have split beside, since that workspace has no group of this
desktop's own. L290 (the host runs and streams the simulator, stablyai#6701) removes the note.

* test(L273-core): the mirrored-workspace simulator test draws the tab and can fail; the side-open test starts from another group

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

* fix(L273-core): list the created-here tab record in the WSL-move storage census

It is keyed by a paired server's environment; a local or WSL project never has one, so a move has nothing to carry.

---------

Co-authored-by: Claude <noreply@anthropic.com>
…nks (stablyai#31963)

* fix(ssh): stop local-only workspace metadata from blocking SSH access links

Unstamped floating and setup terminal metadata rows were projected onto every
execution host, so the dependent-state census refused SSH access links and
empty-host claims for every SSH target with orcad_migration_dependent_state.

- Local-only workspace ids project only onto the local host, are stamped local
  on load, and their metadata writes always land on the local host.
- The census counts an unstamped legacy row only when its repo is not
  registered solely on other hosts.
- The SSH access error now names the blocking state.

Builds on the approach in stablyai#31862 by Frank-zhu2000.

Fixes stablyai#31498

Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>

* fix(ssh): rehome remote-stamped local-only metadata, skip orphan rows, and word the SSH access error for a link

* fix(persistence): keep a local or unstamped floating row over a remote copy when rehoming

* fix(ssh): make the SSH access blocker copy switch exhaustive

---------

Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>
…s for detached tabs (no behaviour change) (stablyai#31970)

* Autosave relies on the guarded write instead of pausing restored or moved tabs

Delete the pendingDiskBaselineVerification and pendingLiveDiskVerification
pause flags. Every editor save is already a guarded write; a refusal raises
the changed-on-disk banner, which is what stops autosave until an explicit
save.

A guarded write now succeeds when disk changed but already holds exactly the
text being saved (local/host compare is byte-exact; client-side fallbacks
compare decoded text and refuse when it holds U+FFFD), so a restored draft
that matches disk goes clean instead of showing a banner.

The restore scan only raises the banner early for tabs that arrive dirty; a
watch resync no longer re-checks dirty tabs.

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

* fix(editor): skip guarded saves when disk is already identical

Return a compatible optional no-op result from hosts and client fallbacks, clean the tab, and clear its provisional self-write stamp without rewriting identical bytes.

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

* fix(editor): refuse lossy automatic saves, keep identical-skip on older hosts, flag dirty tabs on resync

- Autosave and pre-restart saves refuse when disk text can't be decoded losslessly, so a
  baseline that decoded to U+FFFD can't authorize overwriting different bytes.
- New files.write.identical-noop.v1 capability; for hosts with only the v1 guarded write the
  client compares first, skips identical saves, then delegates to the host's guarded write.
- Watch resync compares dirty tabs against disk and shows the changed-on-disk banner.
- Local guarded writes check for identical content before the write-only regular-file probe.

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

* fix(editor): tell non-UTF-8 files apart from disk changes, and quiet false resync banners

- Local guarded writes judge losslessness by bytes, so a valid UTF-8 file containing U+FFFD
  autosaves again. A file that really isn't UTF-8 is refused with its own reason, 'not-utf8'
  (older clients read it as 'changed'), which pauses autosave for that file with one notice
  until an explicit save converts it.
- A watch resync whose disk read fails leaves dirty tabs alone; the guarded save still catches
  a real conflict.
- Resync and restored-tab checks treat disk holding the tab's own just-written save as an echo.
- On hosts with only the v1 guarded write, only automatic saves read the file first; explicit
  saves go straight to the host's guard.

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

* fix(editor): lift the non-UTF-8 autosave pause when its edits end, and report disk changes first

- The pause is released once no dirty tab of the file remains (close, reload from disk,
  discard), so a reopened or reloaded file's next autosave asks the host again and notifies.
- Client-side precondition checks compare the signature before looking for U+FFFD, so an
  external rewrite shows the changed-on-disk banner rather than the non-UTF-8 notice.

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

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…nd Linux packages (stablyai#31946)

* fix(packaging): keep ssh2's Windows-only Pageant files out of macOS and Linux packages

* test: explain the win32 ssh2 filter fixture and format

* test: read the excluded ssh2 file names from the exclude table
…ead of silently skipping tests (stablyai#31981)

* chore(test): run-vitest launches the pinned Bun, and refuses a Bun older than config/.bun-version

An older Bun silently runs no tests in files that reach newer node builtins, so a local
comparison could skip them on both sides.

Claude-Session: W5b

* chore(test): an older Bun ends run-vitest by exit code, so its message is not cut off

Claude-Session: W5b
* Autosave relies on the guarded write instead of pausing restored or moved tabs

Delete the pendingDiskBaselineVerification and pendingLiveDiskVerification
pause flags. Every editor save is already a guarded write; a refusal raises
the changed-on-disk banner, which is what stops autosave until an explicit
save.

A guarded write now succeeds when disk changed but already holds exactly the
text being saved (local/host compare is byte-exact; client-side fallbacks
compare decoded text and refuse when it holds U+FFFD), so a restored draft
that matches disk goes clean instead of showing a banner.

The restore scan only raises the banner early for tabs that arrive dirty; a
watch resync no longer re-checks dirty tabs.

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

* fix(editor): skip guarded saves when disk is already identical

Return a compatible optional no-op result from hosts and client fallbacks, clean the tab, and clear its provisional self-write stamp without rewriting identical bytes.

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

* fix(editor): refuse lossy automatic saves, keep identical-skip on older hosts, flag dirty tabs on resync

- Autosave and pre-restart saves refuse when disk text can't be decoded losslessly, so a
  baseline that decoded to U+FFFD can't authorize overwriting different bytes.
- New files.write.identical-noop.v1 capability; for hosts with only the v1 guarded write the
  client compares first, skips identical saves, then delegates to the host's guarded write.
- Watch resync compares dirty tabs against disk and shows the changed-on-disk banner.
- Local guarded writes check for identical content before the write-only regular-file probe.

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

* fix(editor): tell non-UTF-8 files apart from disk changes, and quiet false resync banners

- Local guarded writes judge losslessness by bytes, so a valid UTF-8 file containing U+FFFD
  autosaves again. A file that really isn't UTF-8 is refused with its own reason, 'not-utf8'
  (older clients read it as 'changed'), which pauses autosave for that file with one notice
  until an explicit save converts it.
- A watch resync whose disk read fails leaves dirty tabs alone; the guarded save still catches
  a real conflict.
- Resync and restored-tab checks treat disk holding the tab's own just-written save as an echo.
- On hosts with only the v1 guarded write, only automatic saves read the file first; explicit
  saves go straight to the host's guard.

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

* fix(editor): lift the non-UTF-8 autosave pause when its edits end, and report disk changes first

- The pause is released once no dirty tab of the file remains (close, reload from disk,
  discard), so a reopened or reloaded file's next autosave asks the host again and notifies.
- Client-side precondition checks compare the signature before looking for U+FFFD, so an
  external rewrite shows the changed-on-disk banner rather than the non-UTF-8 notice.

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

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

* feat(editor): views of one file share one buffer record

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

* fix(editor): review fixes for shared buffer records

A dirty view with no draft no longer pushes disk text into the shared model; merge and
deleted-banner joins mirror the record; web-session sync honours detached views.

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

* feat(editor): shared records take every writer, rejoin, outlive no-prompt closes, restore merges

- Rich Markdown, notebook and CSV commits also edit the record's models no editor shows, as one
  undoable edit; a save moves every loaded view's panel text to the written disk text, so a stale
  dirty check can't mark the record clean over unsaved edits.
- A detached pair rejoins once one side is clean (save, Discard, reload) or the partner closes:
  the clean side attaches, a dirty side wins with its own baseline, so its next save is refused
  and the banner shows on every view. Rejoin runs on the next microtask, after the caller has
  finished settling the record; doing it mid-settle let the settle clear the adopted draft.
- A dirty record whose last view a no-prompt removal closed is kept with its draft and model;
  reopening the file joins it, it never autosaves, and dismissing the removal notice drops it.
- Restored identical drafts merge; divergent ones detach with their own models and timers.

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

* test(editor): cover shared buffer records end to end, and restart-save a record through its writable view

Adds save-queue and store tests for rename and owner-resolution re-keys carrying a conflict, a
read-only sibling joining a dirty record, retired-runtime quarantine, runtime-in-SSH identity, the
reconnect writer, restart once per record, and a 200-tab index.

Fix: restart picked a record's first dirty view, so a read-only view listed first refused the
whole restart; it now picks the record's first view that may autosave.

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

* fix(editor): G3 review fixes for kept records, rejoin, undo steps and save events

- A no-prompt removal's keep request rides in the write that closes the tabs
  (editorBufferKeepRequest, consumed by the record middleware), so a removal
  closing tabs over several writes keeps every dirty record it closes.
- Kept records carry their removal notice's token; dismissing a notice, or
  failing to show it, releases only that notice's records.
- A divergent dirty view reopening a kept record stays detached on its own
  model; a rename onto a kept record's path drops the kept record.
- Each rich/notebook/CSV commit into a retained model is its own undo step.
- The saved event names the record's views, so the panel updates them even
  when the saving tab closed mid-save.
- A queued save compares the discard epoch of the record it was queued under.
- Typing no longer runs the rejoin check; only clearing a side's draft does.

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

* fix(editor): keep open-file mirror writes out of the layout writer ratchet

The record middleware now applies reconcile's per-view mirror fields as a
membership-preserving map over openFiles, and the mirrored-dirty adoption
returns its views for the session-sync patch to write, as it did before.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…yout stays open (switch-on builds only) (stablyai#31898)

* fix(L273-core): tabs this desktop created in a paired server's frame-drawn workspace survive the server's frames (switch-on)

A tab the server's layout never lists (a client-hosted browser tab today; any kind recorded the
same way) used to vanish on the server's next frame rebuild. This desktop now records such tabs by
server and workspace, in its localStorage only (never a session field nor sent), and the server's
frame builds keep them as drawn, inside the server's own groups:
- A held tab drawn in a group the server lacks moves to the nearest server group; it shows there
  only if the user was on it, and the user's group follows it. The group tree stays the server's.
- The record forgets an id once the server's frame lists it, on close, and on worktree removal,
  and prunes ids the restored session no longer has before the first frame after a reload.
- A held tab's drag moves it here only (no session.tabs.move); it offers no new split, and a
  server tab sharing its group counts only the server's tabs for split no-ops.
- Client-hosted staging records its tab (both ids) in layout-owner builds; restage takes the
  worktree from its caller instead of scanning the store.

* refactor(L273-core): the desktop-created record is inert with the switch off; clearer names

- With the switch off the record is never read, kept or written (a single gate in the record).
- The frame-drawn check is isDesktopCreatedTabInFrameDrawnServerWorkspace in
  frame-drawn-desktop-created-tabs.ts: "held" stays the frame hold's word.
- The held-tab placement drops its unread activeGroupId; the group follow derives it.

* fix(L282): a client-hosted browser tab this desktop creates in a frame-drawn server workspace completes once the server's snapshot lists it, instead of undoing itself after 8 s (switch-on)

* fix(L284): a server-hosted browser tab this desktop creates in a frame-drawn server workspace completes when the server's frames take it over, instead of reading as the user's close (switch-on)

* chore(L282/L284): adopt only staged client-hosted handles, and name the frame build's handover note (review nits)

* fix(L284): a staged tab the server's frames took over completes the create though its pane, unmounted, has no handle yet (layout-owner build only)

* fix(browser): a switch-on host answers a paired client's browser create with the new page's id, not its workspace's, before main's frame draws it

* fix(browser): the reply names the page id the create asked for first, then the new record's own page (L291)

* fix(L284): the frame build scans dropped pages for a handover only when a staged create's page is drawn, so a large close stays one pass
… (read-only) (stablyai#31990)

* Autosave relies on the guarded write instead of pausing restored or moved tabs

Delete the pendingDiskBaselineVerification and pendingLiveDiskVerification
pause flags. Every editor save is already a guarded write; a refusal raises
the changed-on-disk banner, which is what stops autosave until an explicit
save.

A guarded write now succeeds when disk changed but already holds exactly the
text being saved (local/host compare is byte-exact; client-side fallbacks
compare decoded text and refuse when it holds U+FFFD), so a restored draft
that matches disk goes clean instead of showing a banner.

The restore scan only raises the banner early for tabs that arrive dirty; a
watch resync no longer re-checks dirty tabs.

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

* fix(editor): skip guarded saves when disk is already identical

Return a compatible optional no-op result from hosts and client fallbacks, clean the tab, and clear its provisional self-write stamp without rewriting identical bytes.

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

* fix(editor): refuse lossy automatic saves, keep identical-skip on older hosts, flag dirty tabs on resync

- Autosave and pre-restart saves refuse when disk text can't be decoded losslessly, so a
  baseline that decoded to U+FFFD can't authorize overwriting different bytes.
- New files.write.identical-noop.v1 capability; for hosts with only the v1 guarded write the
  client compares first, skips identical saves, then delegates to the host's guarded write.
- Watch resync compares dirty tabs against disk and shows the changed-on-disk banner.
- Local guarded writes check for identical content before the write-only regular-file probe.

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

* fix(editor): tell non-UTF-8 files apart from disk changes, and quiet false resync banners

- Local guarded writes judge losslessness by bytes, so a valid UTF-8 file containing U+FFFD
  autosaves again. A file that really isn't UTF-8 is refused with its own reason, 'not-utf8'
  (older clients read it as 'changed'), which pauses autosave for that file with one notice
  until an explicit save converts it.
- A watch resync whose disk read fails leaves dirty tabs alone; the guarded save still catches
  a real conflict.
- Resync and restored-tab checks treat disk holding the tab's own just-written save as an echo.
- On hosts with only the v1 guarded write, only automatic saves read the file first; explicit
  saves go straight to the host's guard.

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

* fix(editor): lift the non-UTF-8 autosave pause when its edits end, and report disk changes first

- The pause is released once no dirty tab of the file remains (close, reload from disk,
  discard), so a reopened or reloaded file's next autosave asks the host again and notifies.
- Client-side precondition checks compare the signature before looking for U+FFFD, so an
  external rewrite shows the changed-on-disk banner rather than the non-UTF-8 notice.

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

* Schedule editor autosave per buffer record, and add the join rules the sharing switch uses

First of the stacked sharing-switch PRs (G1). Inert while every record has
one view, which is all main produces today; tests drive the shared shape
through hand-built view indexes.

- Autosave timers are decided per record by its first view that may
  autosave, so a read-only or preview view of a record no longer cancels
  the record's timer, and a timer survives the view that scheduled it
  closing (it resolves a live view of the record when it fires).
- A view of a detached record gets its own model owner key, so divergent
  dirty drafts never share text with the file's other record.
- Pure join and rejoin rules: rekey / attach / adopt (dirty wins) / merge
  (identical dirty) / detach (divergent dirty); a detached pair rejoins
  only once one side is clean.
- Mirrored host tabs: a record's dirty flag is the OR of its host-owned
  views, since an older host publishes it per tab; the dev invariant
  tolerates exactly that disagreement.

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

* Keep pauses and pending text out of buffer merges, and pick a writable view for autosave

- A merge needs equal drafts, baselines, conflict and owner-migration state;
  anything else detaches, so a paused record never resumes autosave by joining.
  A side whose text is still pending (no draft) never counts as identical.
- A clean-flagged view that still holds a draft counts as dirty in joins.
- The invariant tolerates a clean host-owned view on a dirty record only when
  another host-owned view of that record reads dirty.
- Autosave and restart skip CSV-preview and read-only views when picking the
  view a record writes through.

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

* feat(editor): views of one file share one buffer record

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

* fix(editor): review fixes for shared buffer records

A dirty view with no draft no longer pushes disk text into the shared model; merge and
deleted-banner joins mirror the record; web-session sync honours detached views.

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

* feat(editor): shared records take every writer, rejoin, outlive no-prompt closes, restore merges

- Rich Markdown, notebook and CSV commits also edit the record's models no editor shows, as one
  undoable edit; a save moves every loaded view's panel text to the written disk text, so a stale
  dirty check can't mark the record clean over unsaved edits.
- A detached pair rejoins once one side is clean (save, Discard, reload) or the partner closes:
  the clean side attaches, a dirty side wins with its own baseline, so its next save is refused
  and the banner shows on every view. Rejoin runs on the next microtask, after the caller has
  finished settling the record; doing it mid-settle let the settle clear the adopted draft.
- A dirty record whose last view a no-prompt removal closed is kept with its draft and model;
  reopening the file joins it, it never autosaves, and dismissing the removal notice drops it.
- Restored identical drafts merge; divergent ones detach with their own models and timers.

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

* test(editor): cover shared buffer records end to end, and restart-save a record through its writable view

Adds save-queue and store tests for rename and owner-resolution re-keys carrying a conflict, a
read-only sibling joining a dirty record, retired-runtime quarantine, runtime-in-SSH identity, the
reconnect writer, restart once per record, and a 200-tab index.

Fix: restart picked a record's first dirty view, so a read-only view listed first refused the
whole restart; it now picks the record's first view that may autosave.

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

* fix(editor): G3 review fixes for kept records, rejoin, undo steps and save events

- A no-prompt removal's keep request rides in the write that closes the tabs
  (editorBufferKeepRequest, consumed by the record middleware), so a removal
  closing tabs over several writes keeps every dirty record it closes.
- Kept records carry their removal notice's token; dismissing a notice, or
  failing to show it, releases only that notice's records.
- A divergent dirty view reopening a kept record stays detached on its own
  model; a rename onto a kept record's path drops the kept record.
- Each rich/notebook/CSV commit into a retained model is its own undo step.
- The saved event names the record's views, so the panel updates them even
  when the saving tab closed mid-save.
- A queued save compares the discard epoch of the record it was queued under.
- Typing no longer runs the rejoin check; only clearing a side's draft does.

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

* feat(editor): conflict-review overview shows the file's shared record, read-only and live

The overview of a conflict review now reads each file through any open view of its
shared buffer record (this workspace's first), so it shows the record's current unsaved
text and model and follows typing in another workspace. It stays read-only and is never
a view itself: it loads as that view would and never pushes text. With no view open it
still shows disk text.

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

* fix(editor): conflict-review overview follows its borrowed view and never acts as it

- The overview's load reruns when the view it reads through opens, closes, re-keys,
  detaches or rejoins in any workspace, so it no longer sticks on "Loading...".
- Retry reloads through the borrowed view's own workspace, and the editor resolves
  through that workspace, so this workspace never re-owns the other's tab.
- Overview editors are passive mirrors: they don't move the borrowed tab's cursor
  line or take a reveal meant for it.
- This workspace's own tab (even a detached one with its own edits) wins over
  another workspace's view.
- The panel's draft selector re-resolves when tabs or record views change, not only
  when drafts do; overview lookups are memoized per tabs/views/routes, and the
  stand-in cache evicts least recently used entries instead of clearing.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…dy exited (stablyai#31968)

* fix(terminal): settle a restored remote pane whose host session already exited

A stale-handle or no_connected_pty refusal kept retrying forever because nothing
read the host's own exit record. On those refusals (and the restore attach's
stale branch) ask terminal.show once; settle the pane as exited only when the
host's verdict is exited, or an older host records an exit cause for a
disconnected terminal. Anything else keeps today's retry.

Fixes stablyai#31539

* fix(terminal): ignore an unverified stop and keep the host's exit code

* fix(terminal): settle on a host exit only when the host holds no replacement pane

* fix(terminal): settle only while the host pane still holds the exited process

* fix(terminal): leave a stopped host surface to materialize instead of settling it
… server workspace go to the server's layout (switch-on builds only) (stablyai#31714)

* wip(h5c-1): server workspace layout intents go to the server's main; positions per stream

* wip(h5c-1): e2e without a cast

* fix(h5c-1): a routed move never moves here; routed callers read the server's stream; a restarted main starts its positions over

* fix(h5c-1): layout-apply-send back under its line limit

* test(h5c-1): a routed answer reaches the handler with the server's stream key

* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did

* feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build)

* fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build)

* feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build)

* test(terminal): tests that counted the window's create calls or publications see main's start of a new tab

* fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197)

* test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252

* fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup

* test(terminal): a Claude account-switch restart's tests draw the pane main holds

* fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build)

* fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice

* fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class

* fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build)

* chore(terminal): mark the phone-close row drop interim (ledger L260)

* test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build)

* test(worktree-create): record main's starts without a type assertion

* fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build)

* fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build)

* feat(h5c-2): a paired desktop's writes to a server workspace its frames draw land in that server's main (layout-owner build)

- Routes setTabProps, setChatPane and non-terminal renameTab to the server's main as layout.apply;
  closes, starts, sleeps and terminal names keep the RPCs that main now applies (session.tabs.close,
  terminal.close, terminal.split, session.tabs.createTerminal, terminal.rename). A window-style
  closeTabs is never routed: main takes it as already stopped and stops nothing.
- A close the server refuses (a pinned tab) redraws the workspace from the server's frames, so the
  window's optimistic drop never outlives the answer; same for a refused pane close.
- A frame-drawn workspace keeps none of the mirror's ledgers (L240): no tab-id mappings, close or
  focus intents; they stay only for servers without layout.intents.v1 until the Phase 2 cutoff.
  Its pane-geometry push is skipped (it goes as intents); pane renames still push.
- e2e: paired-desktop-server-layout-writes covers close (layout, session.tabs, terminal stop),
  rename, split and new tab on a windowless switch-on serve.

* fix(h5c-2): an undelivered close of a frame-drawn tab is held for replay; a launch-agent clear stays off the server route

* refactor(h5c-2): the undelivered-close test moves to its own module, keeping the lifecycle file under its line cap

* fix(h5c): a routed tab move anchors on the server's tabs only, and never sends an anchor the server does not hold (layout-owner build)

* fix(h5c): every routed tab move is placed among the server's tabs at the routing point, skipping desktop-only tabs (layout-owner build)
Upstream range 5f728f7..ec5eaf9, 3,484 commits. 94 conflicted
files, resolved per fork feature. Upstream moved src/relay to
src/wsl-guest, src/shared/child-process to @orca/process-host, and
sidebar grouping to src/shared/sidebar-list, split runtime construction,
and deleted several files the fork had edited.

- Config, CI, docs: electron.vite.config.ts (fork update defines moved
  to config/build-plugins/swaplabs-update-defines.ts for max-lines);
  pr.yml (sync-PR diff base on upstream's new gate steps); AGENTS.md
  (fork section appended to upstream's rewrite, with a resume-doc
  pointer); .gitignore allow-list; build-constants.d.ts; locales (union
  of keys; the new pt.json gets the fork title). Upstream-deleted test
  inventories taken as deleted.
- Update sources (#8 #10 #13-#17 #27): ported onto upstream's updater
  rework; refusal builders and a feed fixture moved to new modules for
  max-lines; the #17 help link moved into the server row details;
  mac-self-update and swaplabs-* scripts now use @orca/process-host.
- Daemon drain (#25): drain routing in daemon-router-drained-daemon.ts;
  hand-over seam in daemon-endpoint-handover.ts for max-lines; orcad
  cache cleanup keeps drain slots; a stale severed AppImage daemon
  drains; no drain on the macOS launchd daemon.
- Worker release (#34): closing-turn wait typed per release mode;
  structured-worker-handle import moved to shared.
- Unreachable-host removal (#3): ported onto upstream's removal flow
  through project-removal-owner.ts.
- Delegated worker nesting (#4): moved into upstream's shared row plan
  (sidebar-list/delegated-worktree-nesting.ts); notice rows follow
  upstream's cross-project lineage rule.
- Resume (#1 #5 #6 #9 #11): retention ported onto upstream's shared
  resume-record rule; scan coverage carried through every scan path;
  the SSH relay scanner removed with upstream's.
- Terminal-loss resume (#30): upstream's record-based resume runs first
  and the hook-store resume is the no-record fallback; upstream's boot
  sweep marks terminal loss; upstream's new clear paths still drop the
  row; a user-ended exit is recorded and only the session's owner can
  end it. The WSL relay port moved to src/wsl-guest. The deleted
  agent-status-store.md gives way to docs/reference/swaplabs-agent-
  resume.md, and upstream's hydrate-order fix replaces the fork's.

Fork lines adjusted for upstream's new gates: workspace-record-read
baseline, a type assertion, a template literal, an effect, and two
daemon files over max-lines.

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

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T16:46:36.146342Z 69e3ed0 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…mt check

Upstream's static-analysis job now runs `oxfmt --check .`, which covers this
fork doc; the sync changed nothing else in it.

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

Copy link
Copy Markdown

💡 Codex Review

// Why: replaying the ended process's cached row after a reconnect would make it resumable again.
if (isCachedRowOfEndedSession(state, notice)) {
this.server.clearPaneState(notice.paneKey)
}
this.forward(notice)

P1 Badge Preserve user-ended notices across relay reconnects

When a WSL hook is accepted while the producer queue is blocked and the host disconnects before redelivery, these lines delete the pane's cached status before the user-ended notice is durably replayable. publishAgentHookUserEndedSession drops notices with no active client, and its retry path assumes reattach replay will catch detached clients, but replayCachedPayloadsForPanes now has no row to send. The restarted host can therefore retain its previously persisted resumable row and automatically resume a Claude session the user explicitly quit after the next terminal loss; retain a replayable user-ended tombstone until delivery can be confirmed.

AGENTS.md reference: AGENTS.md:L83-L85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kiwidream
kiwidream merged commit 23cb211 into swaplabs/main Oct 22, 2026
129 of 139 checks passed
@kiwidream

Copy link
Copy Markdown
Author

Merged at the operator's request with these checks red on 69e3ed0821. The causes, read from each log:

  • Preflight critical-path comparison (smoke, sync-spawn, typecheck), Preflight comparison (compilers, compatibility-imports), Release JavaScript comparison:
    • These are upstream benchmark workflows, triggered only because the sync touches their files.
    • They compare the head with its first parent, which for a sync is the old fork. That is why sync-spawn fails with "Cannot read baseline config/scripts/check-main-sync-spawn-ratchet.mjs".
    • Typecheck was killed (exit 143). Smoke's warm-up failed compiling node-pty on the ARM runner.
    • Release JS found 28 renderer locale assets that differ on win32 only.
    • Upstream's own runs of these workflows fail often too.
  • Cross-version wire compatibility: the step "Runtime launcher change does not bump daemon protocol" diffs the whole three-week range against the old fork, so it includes upstream's own launcher and protocol changes.
  • WSL orcad E2E: "Import the distros as WSL2" timed out after 10 minutes, before any test ran. On the first head, the real-WSL-terminal job hit a network timeout in setup and passed on the re-run.
  • PR checks passed: the aggregate of the above.

The one failure that came from the fork's own lines, the formatting of docs/reference/swaplabs-fork-builds.md under upstream's oxfmt --check ., was fixed in 69e3ed0821. Static analysis and typecheck passed on that head. Codex finished on 69e3ed0 with no findings, and Tenki had not reviewed when the PR merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants