* fix(floating-workspace): reveal workspaces where they live instead of selecting the floating one
Every renderer path that takes the user to a workspace hand-wrote the main-window
selection sequence, so a floating workspace id (terminal switch, CLI focus, agent
launch, notification and mobile reveals, terminal handle links) became the main
window's selection and blanked it while the floating panel stayed closed.
revealWorkspaceSurface is now the one way to show a workspace: the floating
workspace opens its panel and leaves the main window alone, any other workspace
becomes the main window's selection and a visit. selectWorkspaceSurfaceOwner is
its non-navigating twin for tabs opening beside the user. The ad-hoc floating
checks in worktree activation, http links, the editor close queue, Activity and
open-terminal-beside-tab now go through the same module.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(notifications): bind clicks for floating and folder workspace terminals
A terminal notification only bound its click when the worktree id carried a
repo ("repoId::path"), so floating-workspace and folder-workspace terminal
notifications did nothing when clicked. ui:focusTerminal now reveals any
workspace where it lives, so the click binds whenever there is a pane to focus
and ui:activateWorktree is sent only when a repo exists.
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* refactor(floating-workspace): tighten the workspace surface reveal
Move the floating-owned check into floating-workspace-panel-reveal as
revealFloatingWorkspacePanelFor so worktree activation no longer depends on
workspace-surface-reveal, rename selectWorkspaceSurfaceOwner to
selectWorkspaceSurface with a named state type, reuse the panel visibility
selector, and flatten the IPC bridges' surface-owner branches.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): leave the floating setting alone before settings hydrate
Host reveals (CLI focus, mobile, notification clicks) can arrive before
settings load; re-enabling the floating feature then would overwrite a
saved `false`. Unhydrated settings now skip the reveal.
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* refactor(floating-workspace): drop extra reveal concepts
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
* fix(floating-workspace): keep main's setActiveWorktree call shape for plain reveals
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
---------
Co-authored-by: Velm14 <10477408+Velm14@users.noreply.github.com>
Co-authored-by: YuriNachos <19365375+YuriNachos@users.noreply.github.com>
* fix(ssh): migrate released profiles with retained client state
* fix(ssh): reuse the shared adoption rule when the local and host copies overlap
The host copy replaces the older local one only where it holds tabs, the
catalog places the workspace on that host, no other host claims it, no
worker fence or close record disagrees, and local drafts are kept. Elsewhere
both copies export and must agree, as on main.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(terminal): show a host-created setup split in the window that holds its tab (STA-9417)
Main only revealed a split of a saved tab when its copy of the window's graph
listed the source pane. That copy lags the tab the create reveal just added, so
the window never drew the setup split and its first-activation sweep left the
setup terminal with no pane or minted a second tab for it. Reveal unless the
window's graph shows the source pane running a different terminal, and reject
a split into an unknown tab before the window switches workspace.
* refactor(terminal): return only the split-source fields a caller reads
* test(terminal): explain the internals cast in the projected split-source test
* fix(terminal): keep a host-made terminal's title across a serve restart
A title given to terminal.create was kept only on the live process record, so
after an orca serve restart the tab read "Terminal N" and terminal.list
listed no title. The runtime now writes it as the tab's saved name when it
admits the pane, and terminal.list falls back to that saved name for a
terminal that has no title of its own yet, as a window does.
* test(e2e): record the kept terminal titles in the layout-compat baselines
* fix(terminal): save a created terminal's name only when the window would show it
A background create (a worker the window never reveals) keeps its live title:
a window reads a saved name on hydration and shows it over the agent's own
title. Also corrects two comments and drops a key count.
* fix(runtime): stop a project's terminals before the host forgets the project
Removing a project on a runtime host (an SSH-managed or paired server) sent repo.rm first; the
host resolves terminal.stop through the project's workspace rows, which repo.rm deletes, so the
stop failed and the shells kept running on the host. Terminals stop first now. The removal toast
also drops the IPC wrapper from its message.
* fix(runtime): only stop host terminals when the host will accept the project removal
Probe with repo.show (same resolver as repo.rm) so a removal the host rejects, such as an
ambiguous project id, no longer kills the shells first. Also poll through terminal_not_writable
in the paired-client e2e.
* test(e2e): read the host shell pid through sh so non-POSIX login shells work
* fix(terminal): keep the tab when a reattach finds its process gone
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): read the paired retention heap after two collections
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): clear the absent owner through the replaced-pane binding writer
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): fail the paired retention check when the heap cannot be read
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(terminal): refuse the respawn without a Store; tighten the binding fixture and stop docs
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(layout): save terminal rows in the tab groups' order
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): read the paired retention heap after two collections
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): fail the paired retention check when the heap cannot be read
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(layout): name the headless flow behind the row-id group fallback
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: upgrade terminal dependencies to the matching payload betas
* test: harden xterm payload contract, align mobile pins, fix serialize-at-ref across version bumps
- Cover foreign-payload replacement and the shared blank-cell reset in the
installed image payload contract (both mutants previously survived).
- Bump mobile's xterm, webgl and unicode11 pins to the root's betas.
- Resolve the ref's own serialize version and patch so the differential
fuzz baseline builds across a version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step.
* ci: ratchet synchronous child-process launches in main and shared code
The Electron main process freezes every window while a synchronous child
process runs, and on Windows each launch is slow because antivirus scans
it. Nothing stopped new synchronous launches from landing in main-process
code, and the spawn observer behind ORCA_MAIN_THREAD_DIAGNOSTICS only saw
async spawns, so the worst blocks were invisible to it.
Ratchet: check:main-sync-spawn-ratchet parses production src/main and
src/shared files (test-only paths excluded) and fails when a file not in
config/main-sync-spawn-baseline.txt reaches execFileSync, spawnSync or
execSync from child_process, runProcessSync from @orca/process-host, or
one of the synchronous secure-file writers (they run icacls on Windows).
Named, aliased, namespace, default, require() and import() forms count.
A baseline entry whose file no longer launches synchronously fails as
stale, so the list can only shrink. It runs in lint and in the PR
static-analysis job next to the process-host import check.
Observer: runProcessSync now reports to the spawn observer, timing the
whole call (that is how long the thread was stuck) and reporting from a
finally so failing, throwing and timed-out children are counted too.
Runtime behavior is unchanged except that the diagnostics observer now
sees synchronous launches; with no observer registered the sync path
pays one extra branch.
* ci: count sync launches per file and gate the repo's sync wrappers
The sync-spawn ratchet only tracked which files launched synchronously, so a
file already on the list (wsl.ts, win32-utils.ts, git-exec-file.ts) could add
more launches unnoticed, and new callers of the repo's own synchronous
wrappers passed CI.
The baseline now stores a per-file count of uses, in the same format as the
focus-read ratchet: a rise or a new file fails, a lower or zero count fails
until `pnpm check:main-sync-spawn-ratchet --prune` lowers it, and --prune
never raises an entry. Every value reference to a gated function counts
(call, hand-off, alias and its uses, re-export); imports alone count nothing.
Gated wrappers: gitExecFileSync (and the git/runner re-export),
restrictWindowsPathSync, hardenSecurePath when called with sync: true,
grantDirAcl, getWslHome, listWslDistros, wslUncDirectoryExists,
isWslAvailable (and the wsl.ts re-export), isPwshAvailable. Their 18
current caller files join the baseline.
Failure messages now give advice per family: the async process-host API for
child-process launches, the async twin for wrappers, and for the secure
writers a note that no async secure writer exists yet, so the sync one stays
and must never become a plain write (that drops the Windows ACL hardening).
Also refresh two stale blockMs descriptions and drop a no-op line from the
spawn-observer "stays silent" test.
* ci: count hardenSecurePath unless visibly async, and tighten ratchet tests
hardenSecurePath was counted only when `{ sync: true }` was written inline,
so moving the options into a variable or a spread dropped the count and the
check then asked for a prune. It now counts every use except a call with no
options or a spread-free object literal whose `sync` is absent or false.
Also: resolve backslash paths in the importing file as well as the
specifier; still visit heritage and instantiation expressions
(`extends mixin(fn)`, `fn<T>`); state the reference-only counting limit;
and point the secure-writer advice at raising this file's baseline count.
Tests cover the hardenSecurePath shapes, the alias chains that need the
fixpoint loop, and a prune that must not raise a grown row.
* test(ci): pin the main sync-spawn ratchet background step
* fix: count secure hardening calls with spread arguments
* perf(notifications): save mobile dismissal history off the main thread
Every agent notification recorded its id in mobile-notification-dismissals.json
through the secure-file writer. On Windows that writer runs about five
synchronous icacls launches per write, so each notification froze every Orca
window (STA-9692).
The file holds notification ids, sequence numbers and session ids, not
credentials, so it now goes through the existing AtomicSnapshotWriter: async
temp-plus-rename, mode 0600, bursts coalesced into one write of the latest
entries. Memory stays authoritative, so delivery and retirement are unchanged.
The pending write is flushed in the quit barrier after chat and RPC teardown,
and in the pre-relaunch hook, since renderer relaunch exits without the quit
pipeline.
* perf(notifications): flush dismissal history when a headless host shuts down
orcad never runs the desktop quit barrier, so its cleanup list awaits the
pending dismissal write after RPC stops.
* fix(persistence): drain a snapshot write requested just after a drain ends
AtomicSnapshotWriter cleared its pending-write promise one microtask after
the drain loop saw no more requests. A write() in that gap set
writeRequested, got the settling promise back, and nothing drained it, so
the newest bytes stayed unwritten and waitForPendingWrite() (the dismissal
store's flush) resolved without them. The drain now clears the pending
promise in the same tick its loop exits, on success and failure, so a later
write() always starts a new drain.
* test(notifications): flush dismissal writes before deleting temp state dirs
Dismissal history now saves asynchronously, and the writer recreates its
directory before each write. Tests that deleted their temp dir with a write
still pending left recreated dirs behind in the system temp folder and could
race the delete on Windows. They now await the controller's flush first.
* perf(notifications): close dismissal history at committed exits
The shutdown flush only waited for writes already queued. A notification
recorded after it (orcad cleanups still running, or the 150 ms before a
renderer relaunch calls app.exit) started an async write the exit dropped,
so the phone could keep an alert the desktop could no longer withdraw.
The store's flush() becomes close(): it waits for the pending write, and
every later record saves synchronously through the snapshot writer's
existing writeSync (plain fs, no icacls; a failure only warns). It is called
only where the exit can no longer be vetoed: the will-quit teardown, a new
exit-only hook that app:relaunch runs before app.exit(0) (restart and
profile switches share onBeforeRelaunch but quit through will-quit), and
orcad's cleanup after RPC stops.
The quit test now holds chat teardown and RPC stop and proves the close
starts only after both settle, in either order. The orcad push-startup
runtime mock gains the method the entry now calls.
* fix(notifications): bound the dismissal close before a renderer relaunch
app:relaunch waited on the dismissal store's close with no limit, so a
stalled userData disk left Restart stuck on "Restarting…" with a half-quit
app saving synchronously. The close now races a 2 s cap through the
existing settleWithinMs helper, the same cap the restart path's auth
preservation uses, and logs when it gives up.
* test(notifications): keep async dismissal writes under test between reads
Mid-test disk reads used closeDismissals() as a barrier, which switched
every later record to the exit-only synchronous path. The store gains a
non-closing flush() (close() = mark closed + flush), exposed on the
controller as flushDismissals for tests; mid-test barriers use it, and
close stays only where a test simulates an exit or teardown. Also notes why
the writer's pendingWrite assignment is safe, renames a store test to what
it covers, and groups the orcad cleanup-order test under a describe.
* fix(ssh): never start a hidden orcad shell for a pane the legacy relay bridge serves
* fix(ssh): a skipped bridged activation still supersedes an earlier tab's pending focus
* fix(ssh): a click on a whole mirrored tab never activates its bridged pane on orcad
* fix(ssh): load the bridge pane list lazily from tab activation
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(ssh): a bridged pane reattaches after a disconnect instead of going deaf
* fix(ssh): a bridged pane asks again when it turns live during an earlier ask
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* wip: legacy relay bridge client
* Revert "fix(ssh): end an older Orca's relay terminals from the connect refusal (STA-10166) (#27748)"
This reverts commit d4a55f583e.
* feat(ssh): bridge terminals an older relay still runs into their old tabs
* test(ssh): cover the legacy relay bridge client, target bridge, transport and gate
* test(ssh): type the bridge test fakes without assertions
* test(e2e): a 1.4.224 relay's live terminal survives the upgrade on a Docker SSH host
* ci(e2e): route legacy relay bridge changes to its Docker spec
* fix(ssh): bridge delta moves too, and never read a non-exhaustive relay listing as exit
* fix(ssh): keep retrying a bridge for panes that mount before connect; end panes proven gone
* fix(ssh): bridge 1.4.224's detached leases through export preflight; publish every leased pane before the first look
* test(e2e): build the released relay out of process so Playwright never loads ESM build tooling
* fix(renderer): drop the recovery hook main removed from PtyTransport
* test(e2e): show the converted worktree before reading the bridged tab
* test(e2e): reconnect and wait for the converted host before opening the bridged tab
* fix(renderer): match bridged leases to tabs mirrored from the managed server
* test(e2e): find the mirrored relay-era tab under any worktree key
* test(e2e): open the converted worktree before looking for its mirrored tab
* test(e2e): a retired lease is terminated or pruned
* fix(ssh): back off attaches a listed terminal refuses; one pane per bridged shell
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Move the 52 non-runtime, non-ipc importers of src/main/providers/types.ts
onto the per-domain contract files. The barrel itself stays until the
runtime and ipc importers move (3.4b/3.4c).
Co-authored-by: m4air <m4air@Mac.localdomain>
* Add host-owned agent launch settings mutations
* Edit agent launch defaults on the selected execution host
* Await durable writes for host agent launch settings
* Fence agent detection by execution-host pairing
* Acknowledge host settings before hook reconciliation
* Preserve Reset after pending agent launch saves
* Simplify agent detection re-pair handling to the clear and the transport fence
A re-pair already clears every detected-agent list, flag and pending probe for
the environment, so the per-record pairing owner keys, the owner-keyed pending
probes, the settle-time pairing checks and the hook's ownership gating were
never reached. Detection now captures the pairing before the capability wait
and fences its RPCs with it; the refresh fallback still refuses after a
re-pair. Pins the fence across a capability wait, the needs-update clear, and
the agent-launch RPC permission levels.
* Open launch settings on the Active Server without the focus-routing helper
Main's new owner-routing ratchet counts getActiveRuntimeTarget calls, and this
pane's no-workspace default added one. It now reads the Active Server field the
way the Agents pane did before host selection, so the default is unchanged.
* Re-probe mounted launch surfaces after a re-pair; rely on the transport fence for the refresh fallback
A re-pair clears an environment's detected-agent lists, but a mounted tab bar,
quick launch or composer that was still waiting for its first list (or whose
first probe had failed) never asked again, leaving "Can't reach this
workspace's host" until another surface mounted. The hook now re-runs its
effect when the target environment's pairing revision changes.
The refresh fallback no longer re-checks the pairing in the renderer: it
already carries the pairing captured before the wait, and main refuses any
mismatch. Pins the Settings host form's remount key on re-pair.
* Share immutable historical source across local cross-version test caches
* Add preview-first cleanup of old historical test caches
* Include historical test cache cleanup in worktree space maintenance
* Report and retain read-only Electron installs during cleanup
* Protect read-only installs under root and isolate cleanup CLI fixtures
* fix(bench): resolve dynamically imported sources as file URLs
`await import(path.join(ROOT, '...ts'))` hands Node a bare Windows path
(`C:\...`), which the ESM loader rejects as an unsupported URL scheme
("Received protocol 'c:'"). The two benchmarks that load production
modules this way crashed on Windows before running a single scenario,
while working on macOS and Linux where an absolute path is a valid
specifier.
Wrap each computed specifier in `pathToFileURL(...).href`, the form the
other benchmark scripts already use for the same purpose.
Verified on Windows: bench:session-write-hot-path and
bench:agent-inspection-cadence now run and print their measurement
tables. The fish mode-2031 trace capture resolves its import too, but its
harness targets macOS/Linux shells and cannot complete on Windows.
Fixes#25235
* style(bench): format the session-write benchmark's file-URL import
---------
Co-authored-by: Paolo <paovalletta@hotmail.it>
Co-authored-by: Orca Worker <orca-worker@localhost>
* test(e2e): record mixed-version, two-client and phone layout baselines on main (P3)
* test(e2e): P3 review fixes: reconnect and I/O proof, restart order bounds, Linux-only baselines
* test(e2e): re-record Linux phone and serve-restart baselines
* test(e2e): P3: echo drawn in the desktop pane; bound the group selection after restart
* test(e2e): re-record serve-restart group selection
* test(e2e): name P3 baseline helpers for what they record
The layout oracle checks the saved profile's round trip at every settled step and the live layout's model rules on every published frame, which covers what this 1 s settle logger did.
* feat(web): the browser signs in with a non-extractable device key, or pairs for this session only
- On a secure page with WebCrypto Ed25519, the browser registers a non-extractable key
(stored in IndexedDB, create-exclusive across tabs) over a v2 token socket, then
reconnects with key auth and replaces its stored pairing code with a placeholder.
Requests on a keyed socket carry no token.
- On a plain-http page on another machine, or without Ed25519, existing stored pairings
keep working unchanged (with a warning); new pairings stay in memory and, when the host
supports it, register a session-only key.
- Only a sealed device_revoked naming this browser's key deletes it; a downgraded host,
a bad proof or host storage trouble keep it.
- The shared handshake core gains an async device-key credential (v2 only).
* fix(web): mark web sockets, bind keys to their pairing, survive a broken key store
- Every web socket carries ?orca-client=web so hosts gate only the web bundle.
- A key record belongs to one pairing revision: a re-pair uses its fresh code.
- A code that never migrated connects without reading IndexedDB; a migrated
pairing whose key store fails retries instead of going dead.
- A tab that finds the key already active forgets its code, and saving a stale
copy of the same pairing never puts the code back over a placeholder.
* fix(web): a stale tab never deletes or overwrites a newer pairing
A key record from a newer pairing revision is left alone (the tab stays on its
code), and saving an older revision never replaces a newer stored pairing.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* test(e2e): open the oracle's headless layout stream with a paired connection
The remote subscription now takes a paired connection rather than a pairing
offer; build one from the offer the way the CLI does.
* test(e2e): build the paired mobile client's connection from its offer
Same stale call as the oracle stream after the subscription and request
functions took a paired connection.
* feat(runtime): session-only browser keys and an opt-in for pairing from plain-http pages (host)
- pairing.registerDeviceKey accepts ephemeral: true (advertised as
pairing.device-key-ephemeral.v1). Such keyed rows are dropped when the host
restarts and after 12 hours with no live socket; a sweep removes them and cuts
any socket still bound to them, so later key auth gets device_revoked.
- The WebSocket upgrade Origin is recorded per socket. A first pairing (a
never-used pairing code) from an http:// page on another machine is refused
with a sealed insecure_origin error unless the operator sets
ORCA_ALLOW_INSECURE_WEB_PAIRING=1. Already-paired browsers, https and loopback
pages, and native clients are unchanged.
- Registry file reading moved to device-registry-file-read.ts to keep the
registry under the size cap.
* fix(runtime): gate first pairings only for the marked web client, never phones
React Native sends an http Origin on every LAN WebSocket, so the Origin alone
refused first-time QR pairing from released phones. The web bundle now marks
its upgrade with ?orca-client=web; only a marked socket from an http page on
another machine is gated. Released hosts ignore the query string.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* test(e2e): read the live runtime layout in the layout oracle
The oracle now reads the runtime's layout from a layout.subscribe consumer
(the window's renderer, or a headless host's pairing offer) instead of
session:get or the saved profile. Model rules run on every published frame,
replacing the 300 ms settle read and the 100 ms rule sampler; disk-format
rules and the round trip run on the saved profile at each settled step.
Views are compared to the live layout exactly for every group and tab kind.
The known-on-main list is refilled from runs of this oracle on main.
* test(e2e): known-on-main differences do not hold the oracle's settle wait
A difference main is known to keep never converges, so a step settles once
only known-on-main differences remain. The orcad title-after-restart entry
is narrowed to the scenario a run on main reproduced.
* test(e2e): oracle settle counts frames across sleeps, pairs renamed groups, flushes saves
The settle wait carries the frame count across reads, so a frame during the
sleep restarts the quiet window. The client check pairs a group listed under
another id with the runtime group holding the same tabs in the same order,
so only that rename is known on main and a dropped group still fails. The
window flushes main's debounced save before the saved-profile read.
* test(e2e): refill known-on-main from Linux CI and post-flush runs on main
Removes the two-writers saved-profile entry (gone once the window flushes
main's save). Re-adds client-reorder tab order and the cold-relaunch client
list, and adds the cold- and live-daemon relaunch tab-bar entries and the
Electron rename-after-restart entry, each reproduced by Linux CI run
38170663248 or a local run on main's code.
* test(e2e): scope known-on-main entries to the OS that reproduced them
Entries reproduced on one OS only carry a platform and match only there.
The cold-relaunch restart entry now matches only a single group whose tabs
and order are unchanged, so a reorder still fails.
* test(perf): in-app layout latency probe, event-loop delay on main and orcad, and a main baseline
* test(perf): plain report comment
* test(perf): name the latency report type
* test(perf): add the Linux CI baseline; routine runs take 10 samples
* test(perf): stamp close pane on the w key, keep probe work out of samples, label first drawn and Linux flood
The host side of the terminal source-range ACK protocol had one producer: the
relay-fed SSH output intake, deleted in #27515. Since then nothing installs the
runtime's source-range hooks and nothing passes source ranges into PTY output,
so every terminal.multiplex subscribe that offers ackOutputSourceRanges is
already answered without it and uses the byte-window ACK.
Delete the ledger, the per-connection registry, the consumer hook surface on
the runtime, the stream-level reservation/commit/rollback plumbing in flow
control, initial snapshot and cleanup, the source-range ACK frame schema, and
the range slicing in output chunks, batching and replay. The subscribe schema
still accepts the ackOutputSourceRanges capability, and the client keeps its
range-ACK support for older hosts. A new golden test pins that an offering
subscribe gets the same reply and frames as a byte-ACK one.
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(remote): one paired E2EE handshake core for desktop, CLI, web and mobile clients
Every paired client now builds its hello, checks the host's ready, seals its
auth frame and opens host frames through src/shared/paired-e2ee-client-handshake.
Platforms inject their own NaCl/base64 primitives. The v2 client session and its
pure-JS key schedule (@noble/hashes 1.8.0, the version mobile pins) move to
src/shared, and the per-host v2 latch policy is shared with an injected store.
No wire change: frames match the captured legacy and v2 fixtures byte for byte,
and a cross-version test drives the core against the released host and the
released client against this host. The auth step is now async so a device-key
signer can slot in later.
* fix(remote): keep @noble/hashes out of the CLI's unbundled runtime
The packaged CLI loads src/shared output from disk, so a bare @noble/hashes
import failed the packaged-runtime check. The v2 key schedule is now a
primitive: Node clients use the node:crypto schedule (moved to src/shared so
the CLI can reach it), while the browser and phone keep the pure-JS one.
* fix(mobile): re-check the v2 latch before sending a legacy auth frame
The auth step is async, so another socket can latch the host to v2 while a
legacy auth frame is being sealed. Refuse it at send time, as before.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(native-chat): a closed chat tab stays closed while the host is still closing it
Closing a chat tab removes it from the window at once. A tab-list frame the
host publishes before it has handled the close (a working chat's status
update, for one) still listed the chat, and the window appended it back at
the end of the strip until the host's own removal arrived.
The window now leaves out a chat whose close it has not heard back on, using
the in-flight close record it already keeps, and re-reads the host's tab list
once the close settles so a chat the host kept comes back.
* fix(native-chat): hide a closing chat tab through the existing close intent
Record a close intent for agent-session:<id> under the chat's host when a chat
tab close begins, so a host frame sent before the host handles the close cannot
re-add the chat at the end of the strip. The intent lifts when a host frame
stops listing the chat, or by its TTL. Only a failed host close clears it and
re-reads that worktree at the version already applied, so the chat the host
kept comes back.
Replaces the close-owner gate and the re-read after every close.
* fix(native-chat): end a chat close intent even when no host frame follows
Re-read the closed chat's worktree after every settled close, not only after a
failure, so a close the host had nothing new to publish for still ends its
intent; otherwise the same chat reopened within the intent's 20 s limit stayed
hidden. Skip the intent for floating-panel chats, whose frames are never
applied. Share one one-worktree local refresh with the Session History reopen
path. Reset close intents between tests in the reopened-tab e2e file, whose
harness tears the host down before a close's follow-up re-read lands.
* fix(native-chat): re-read after a successful chat close only for paired servers
This machine's host emits its removal frame before the close answers, and the
window applies it, so a local chat re-reads only when the close fails. A paired
server's frames travel apart from its answer, so, as for its terminal closes,
every close is followed by a read, and that read no longer joins a tab-list
request that began before the close. Floating-panel chats record no intent and
get no read.
* fix(native-chat): pin a chat close intent to the pairing the close began under
Record and clear the intent with one owner captured at close start, as paired
terminal closes do, so re-pairing a server mid-close cannot miss the clear, and
pass that pairing to the paired re-read.
* refactor(pty): delete the SSH PTY provider and its registry slot
Nothing in production registers an SSH PTY provider, so the registry's SSH
map was always empty: every SSH lookup answered undefined or threw the
'No PTY provider for connection' error. Delete the provider stack, its
consumer-session and source-recovery contracts, the registry maps and the
getSshProvider dependency, keeping each caller's existing result for an
absent provider. The registry now dispatches only 'local'; any other host
throws PtyHostNotDispatchableError, which keeps the renderer-matched
message for SSH ids and is never read as a gone or exited PTY.
* test(pty): keep the no-provider SSH routing cases in local-only form
* test: reword a comment naming the deleted SSH PTY provider
* test: drop an import the rebase left unused
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(runtime): device-key authentication and per-installation key migration (host)
* fix(runtime): corrupt registry is never a revocation verdict; keyed sockets dispatch without a token; forks keep their Relay binding
* fix(runtime): keep an unparseable device registry aside; an unlisted key is never 'revoked' while it exists
* fix(runtime): a set-aside corrupt registry keeps verdicts doubtful even when the registry file is missing
* test(runtime): registerDeviceKey reaches the bound socket through the dispatcher for runtime and mobile rows
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(terminal): panes have two transports, local or remote runtime
* fix(terminal): keep local-only startup gates off unconverted SSH panes
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(mobile): direct connections use v2 framing and never fall back once v2 is seen
* test: drop casts in direct v2 tests
* fix(mobile): count v2 handshake failures against the auth retry budget; latch only after host auth
* fix(mobile): pick direct framing at open, recheck the latch before legacy auth, and keep old hosts off the auth budget
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
The terminal gate's relay lister always resolved to null in production (no SSH PTY
provider is ever registered), so its relay and previous-relay branches, the legacy
relay route/router/routing, the previous-relay census and the once-connected
re-check could never run. The gate now takes only the host census; census-only
verdicts are pinned by a characterization test written before the change.
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
orcad now checks its flags with the same validator Electron serve and the
CLI use, so contradictory pairing flags fail with the same message instead
of one silently winning. The CLI launcher drops its duplicate recipe check
(the CLI handler already ran the shared validator).
Removes the daemon-init and cli/runtime/index re-export barrels and the
parsePtySessionId / OrcadInstanceLockError re-exports; callers and mocks
import the defining modules.
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Removes seven modules that only their own tests import, dead orcad exports
and protocol constants, spells orcad's configuration exit code once, reuses
the shared errorMessage helper, and drops links to the deleted
docs/reference folder.
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The relay client is gone, so the three relay-ladder cells and the
@orcad-cli-relay-kept matrix case could not run. Delete them, their
last helper (ssh-test-target-connection), a stale path filter and the
unused build:relay step; rename the runner to invoke-windows-host-cells.ps1.
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(ssh): remove the relay-only remote workspace sync so reconnect never sticks on pulling
* refactor(ssh): drop snapshot apply tokens and terminal finalization only the sync used
* test: drop leftover sync-status mock
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* wip: delete the SSH relay server and its build, assets and tests
* wip: drop main/relay plugin conformance; WSL reader lookup test
* fix: drop the emptied relay bench; prune baselines
* test: keep the native halves of tests that also covered the relay
* chore(ci): drop shard timings for the deleted relay server tests
* fix: keep the directory listing budget that darwin terminal names now import
---------
Co-authored-by: m4air <m4air@Mac.localdomain>