Commit Graph
2857 Commits
Author SHA1 Message Date
Brennan Benson 1f4a8ed9be perf(agent-map): stream status updates to popout 2026-08-05 18:56:58 -07:00
Brennan Benson 2a6552f27f fix(agent-map): isolate map work from main renderer 2026-08-05 18:19:32 -07:00
Brennan Benson 63173ff12e Merge remote-tracking branch 'origin/main' into brennanb2025/agent-dashboard-ui-revamp 2026-08-05 13:43:44 -07:00
KyouandOrcaWin 74ac7049ec fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset (#11782)
* fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset

Fixes #9358 and #9941.

cmd.exe expands %VAR:~n,m% at parse time. When GROK_HOME is unset (default
outside Orca terminals), the generated length/trailing-backslash guards
became a syntax error and every Grok hook event failed with exit 255.

- Skip substring work when GROK_HOME is undefined (if defined + goto)
- Replace if "%x:~-1%"=="\" (itself a quote-parser bug) with findstr
- Extract Windows script builder; add template + spawn tests

* fix(windows): harden grok-hook GROK_HOME guards and tests

Address review on #11782:
- Inject grokHome via buildWindowsAgentHookPostCommand extra form lines
  (no fragile string replace of the shared payload line)
- Spawn tests delete GROK_HOME and keep PORT/TOKEN/PANE_KEY set so the
  GROK_HOME path actually runs before curl

* fix(windows): cover Grok hook home boundaries

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-08-05 13:31:26 -07:00
Brennan Benson 4c49989c2e refactor(codex): delete the unreachable managed shared-mirror lane (#12614)
PR 9501 shipped real-home routing for the host system default, and the
env override that could turn it back off was never a shipped control. The
managed-account half of the shared runtime mirror has been unreachable
since: every host account routes to its own self-contained CODEX_HOME
before that code runs.

Delete the flag module and its env plumbing plus the managed branch of
syncForCurrentSelection and the six helpers only it called. The three
lanes that still use the shared mirror -- Windows, a custom CODEX_HOME,
and a hook-lane gate that reports unusable -- are untouched, as are every
legacy migration and the WSL read-back helpers.
2026-08-05 12:57:02 -07:00
Brennan Benson 38ba22ecd1 fix(browser): align cookie import safeguards (#12607)
* fix(browser): align cookie import safeguards

* fix(browser): preserve sessions on failed cookie imports

* fix(browser): bound single-label cookie replacement

* fix(browser): preserve host-only parent cookies

* fix(build): bundle cookie scope parser
2026-08-05 12:51:05 -07:00
Brennan Benson 5d2ad3597a fix(native-chat): add direct Codex model selection (#12657)
* fix(native-chat): select Codex models directly

* fix(native-chat): confirm agent exits before switching views

* fix(runtime): handle unavailable foreground probes
2026-08-05 11:27:12 -07:00
Brennan Benson de64337c26 fix(worktree-watcher): refresh status after external pushes (#12361)
* fix(worktree-watcher): surface external push -u through the git-common watch

An external-shell 'git push -u' writes only the common .git/config (plus
refs/remotes/<remote>/<branch>), both invisible to the git-common event
filter, so the Checks panel stayed on 'No upstream configured' until the
renderer safety poll. Classify the common config and remote-tracking refs
as status-tier signals, poll config alongside the other primary-checkout
metadata files, and keep FETCH_HEAD/reflog/ref-lock churn ignored.

* fix(worktree-watcher): refresh after subsequent pushes
2026-08-05 11:26:13 -07:00
6942871194 feat(editor): add Markdown table structure controls (#11985)
* feat(editor): add markdown table structure controls

* fix(editor): scope table context actions to cells

* Replace table toolbar with context-aware overlay controls

Replace the fixed table toolbar with context-sensitive overlay controls that position themselves around the active table, adding support for direct row/column insertion and full table deletion. This approach is less intrusive and supports click-targeted actions via coordinate-based cell resolution. Enhance structural safety by preventing header removal and ensuring tables never collapse below a single cell, deleting instead when the final row or column is removed. Harden the context-menu query with a 120ms timeout to keep the native menu responsive even if the renderer hangs.

* Replace markdown table context query with IPC coordination

Capture table cell targets on pointerdown and report via IPC channel
instead of executing JavaScript on context-menu events. Eliminates
120ms query timeout and unavailability race conditions. Header cells
now disable incompatible row-level actions.

* fix(editor): make table column rebalancing atomic with insertion

- Refactor rebalanceAddedColumn to mutate the caller's transaction, grouping
  insertion and rebalance into a single undo step
- Add validation for cached cell positions that may outlive the document
- Fix table detection to use isInTable() instead of isActive('table')
- Correct z-index layering to respect menu stacking context
- Fix cleanup of stale animation frames and pending pointer state

---------

Co-authored-by: rainL <WYK15@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-05 11:10:05 -07:00
Jinjing 9fb4dbe8eb fix(ssh): handle rejected PTY deliveries with targeted recovery (#12746)
Add targeted recovery for rejected PTY source frames instead of
terminating the relay channel. Classify rejection reasons (malformed,
generation mismatch, range invalid) and attempt recovery based on the
rejection type. Implement admission control at publication time to ensure
frames aren't delivered after ownership changes. Bound recovery attempts
and retry with backoff to prevent exhaustion. Diagnose and log rejection
reasons to aid debugging.
2026-08-05 10:30:04 -07:00
Jinwoo Hong eea0bb64db fix(ssh): make PTY owner admission explicit and non-destructive (#12673)
An owner-capable `pty.openClient` had two failure modes that presented as something else.

If the relay still held an owner record but the request carried no matching resume proof, admission fell through to a SUBSCRIBER grant — a success-shaped response the client cannot use, which it then rejected as "did not grant an authenticated PTY session owner". And if the relay had forgotten the record the client named, admission threw a stale-recovery error, which the client answered by deleting its own recovery row — `clientInstanceId` included — and reopening. Two round trips, and the identity that lets it resume that target at all went with the deletion.

Now every owner grant carries a required `resumed` flag, a forgotten record mints a fresh claim in one round trip, a held claim returns one of three coded refusals, duplicate opens on one connection are rejected even when identical, and an attached-holder refusal becomes a typed error routed through the terminal-relay-error callback instead of feeding redeploy backoff a link that is working fine.

Independent review caught two regressions in the first attempt, both now fixed and both with tests that fail without them:

**A backpressure teardown could take a live owner's session.** The safety argument was that a record only becomes `disconnected` from an observed peer close — but two of the six paths there are capacity paths, where the relay destroys the client's socket itself because its lane queue filled. That is the signature of a client that is ALIVE but not draining fast enough. Demonstrated: the real owner is torn down for backpressure, a rival is granted ownership 270ms into a nominal 30s grace, and the owner's later reconnect with a valid resume proof is refused permanently, backoff cleared, no retry. Closes now carry a cause (`peer-closed` | `local`, defaulting to `local`, which only ever widens a grace), and the floor applies only to closes the transport actually observed on the peer's side. Capacity teardowns, decode faults and sink failures keep the default.

**A client's own zombie connection blocked it permanently.** Only `SshRelaySession` ever requests owner, and every endpoint-credential client shares one principal — so in a normal single-app deployment an `active` incumbent refusing you is almost always your own half-open connection the relay never saw close. That was refused as terminal, where main recovered on bounded backoff once keepalive noticed. The refusal already held both client identities; a match is now a distinct transient refusal that falls through to relay-lost backoff, restoring that recovery. A genuinely different client is still blocked.

Also: each retry deadline now starts when its own phase begins, instead of both being computed at entry where a slow first phase could leave the second with zero attempts.

Fixes STA-3365.
2026-08-05 02:56:07 -07:00
Jinwoo Hong cc1859d61c fix(ssh): snapshot detached SSH leases on quit and bound the teardown (#12687)
Per-target SSH teardown awaited `removeAllForwards` BEFORE anything marked the lease detached, so a slow forward close let the final store flush snapshot while leases still said `attached` — and the later durable write was rejected because persistence had already finalized. On the next launch those leases described a state that never existed.

`beginSshShutdown()` now performs every in-memory transition synchronously before returning, and the quit path calls it immediately before `store.flushAsync()` with no await between. The whole drain shares one deadline that REPORTS unfinished `{targetId, phase}` rather than concluding anything about it, and `waitForSystemSshForwardStop` gained a post-SIGKILL bound.

Nothing here destroys a session. `detached` means this app let go of the lease, not that the shell died — the pre-pass exists precisely so still-running PTYs are recorded as detached-but-alive instead of being lost to an `attached` snapshot. Review confirmed every reader honors that: reattach enumeration and persistence restore filter only `terminated`/`expired`, lease normalization has no age-based expiry, and attempt exhaustion leaves a lease alone. The drain deadline's only consumers are a warning log and a join that discards the value — nothing reads it as "gone".

Review also caught a defect the refactor introduced, now fixed: making the pre-pass synchronous meant a throw from `beginShutdownDetach` — via `webContents.send` on a renderer that quit had already destroyed — escaped the non-async `will-quit` listener and skipped `killAllPty()`, the watchers, `store.flushAsync()`, the teardown barrier and `app.quit()`. That would have lost the exact snapshot this PR exists to make correct. Each call is now wrapped per session, collecting errors and continuing. Proven: the test throws from the first of two sessions and fails without the fix with "Object has been destroyed".

A second test could only fail via timeout rather than assertion; the ordering is corrected so removing the post-SIGKILL bound now fails in 5ms with a clean assertion instead of a 5s timeout.

Rebased onto main and verified independent of #12673 (zero references to its owner-admission changes), which is being reworked separately. Fixes STA-3366.
2026-08-05 02:54:29 -07:00
Jinwoo Hong d15939c5fd fix(terminal): recover rejected paired-runtime input (STA-2830) (#12675)
With a desktop client paired to a remote Orca runtime, terminal panes could report connected, writable, and `terminal.send` returning accepted — yet keystrokes never reached the agent. No error, no banner, no recovery; input silently vanished.

The ticket was really two bugs. The attach half was already fixed by #12589 (subscriber-driven daemon attach), confirmed by reproducing against current main. This fixes the remaining half: a write the host refuses had no way to tell anyone.

A capability-negotiated `WriteUnavailable` opcode carries that refusal back to the client, where it feeds the pane's pre-existing recovery hook. Capability gating matters because decoders reject unknown opcodes on desktop — and, worse, silently drop them on mobile — so the signal is negotiated in the subscribe handshake. Verified per direction: an old host strips the unknown Subscribe key, an old client omits it so the host never emits, and capability cannot be inherited across resubscribe.

Independent review then found the signal was being delivered and discarded: recovery demanded an authoritative liveness answer, and `pty:hasPty` had no `remote:` guard, so a paired pane's id fell through to the LOCAL provider, which returned false, and recovery bailed before remounting. Every test stopped at the transport boundary, so all of them passed while the pane stayed just as stuck. `pty:kill` already had exactly that guard.

The fix makes main answer LESS rather than claim more: `pty:hasPty` now returns unknown for a `remote:` id instead of a fabricated false, because main cannot speak for another host's PTY. The remount is then authorized by positive evidence — the process that owns the PTY stating it refused this specific write over a live negotiated connection — not by inference from silence. Local and app-SSH ids keep the probe, where a false genuinely means the shell died. Nothing is destroyed on this path; the remount rebuilds the renderer over the session it already had.

An end-to-end test now carries a rejected write from the host through to an actual remount, which no prior test did. A surviving mutant was also killed: the legacy-binary capability gate could previously be deleted with nothing turning red.

The reliability gate stays experimental — live paired journeys and mixed installed-release evidence remain uncollected. Fixes STA-2830.
2026-08-05 01:41:27 -07:00
Jinwoo Hong a766ee4bcd fix(runtime): refuse to silently wake a deliberately slept pane (STA-3465) (#12672)
`activateMobileSessionTab` gated only on `publicTab.status !== 'ready'`. A deliberately slept pane publishes as `pending-handle` indefinitely — indistinguishable at that call site from a pane awaiting reconnect — so the reconnect probe added by #11542 respawned it with a re-resolved agent launch, waking something the user had deliberately put to sleep.

The first attempt refused activation for any pane with a `worktree-sleep` record, applied to every path. Independent review found that broke the documented wake gesture: opening the tab IS how those panes are meant to cold-restore (`wake-sleeping-agents-in-background.ts`: "Those panes cold-restore --resume when their own tab is opened"). A mobile tap sends the byte-identical call the reproduction test used, and in three of four topologies no wake clears the record first — so the tap became a permanent no-op with no feedback.

This carries intent explicitly instead of inferring it. A new shared `TabActivationIntent` ('user' | 'automatic') rides the existing ActivateTab schema as an optional additive field; `isAutomaticTabActivation` returns true only for an explicit 'automatic', so an absent value is permissive BY CONSTRUCTION in one place — an older client that does not send it keeps today's behavior rather than silently losing its wake gesture. The field is required on the mobile helper's params, so no call site can be added without declaring who asked.

Every user path (mobile tab switches, paired tab clicks, shortcuts, palette, the pane's own open) is labelled 'user'. The only automatic sender in the codebase is `waitForResubscribeHostSessionHandle`, the #11542 reconnect probe.

Verified per topology: user activation materializes a parked pane under headless serve, a paired runtime client, a completed agent with restoreOnTabOpenOnly, and a running agent whose wake cleared the record. The automatic probe is refused without retiring the surface, and #11542's reconnect tests stay green.

Also fixes a test fixture that made a real bug untestable: the store stub ignored the host id, so mutating the partition lookup to 'local' left the suite green. Correcting it exposed three existing SSH reattach tests that had been relying on that looseness — their workspace session sat in the local partition while their repo was SSH-hosted, a store production would never read. Production was always right; the tests described an impossible world.

Fixes STA-3465.
2026-08-05 01:28:07 -07:00
Jinwoo Hong 9accd97bd9 fix(browser): stop an over-limit screencast frame from killing the paired runtime socket (#12680)
Opening any webpage in the remote browser dropped the paired runtime connection, and the client then retried forever without recovering.

Causal chain: the screencast travels host->client, a direction that admits up to 8 MiB. The host's encrypted channel rejects anything larger with close code 1013 "Outbound reply buffer overflow" — killing every subscription on that connection. The producer treats a false return as backpressure and retries the identical frame, which for an over-limit frame can never succeed. A permanent condition was being treated as transient.

Two changes:

1. A paired-runtime admission wrapper: an over-limit frame is dropped rather than handed to the transport, and reported as handled so the producer advances instead of retrying something doomed. The generic Chromium producer is untouched, so local browser behavior is unchanged.

2. The actual source of over-limit frames. Live frames are hard-bounded by maxWidth/maxHeight, but the navigation snapshot path ignored those bounds entirely, feeding capturePage device pixels straight into the encoder — capturePage's rect is CSS pixels while the bitmap is device pixels, so at deviceScaleFactor 2 a snapshot could be 4x the pixel area the live path is allowed to send. That path fires on page load, which is literally the reported trigger. Applying the caller's own clamp there makes the drop a backstop rather than the mitigation.

Dropping a frame is safe here because frames are complete standalone images, not deltas — each replaces the client image wholesale, so the next frame fully repaints. Disclosed in the PR: mobile web-view mode sends no viewport and takes the unclipped screenshot branch, where the drop guard remains the only protection; still strictly better than a 1013 that kills every subscription.

Verified by reverting in place: neutralizing the admission guard fails 3 oracles, with the integration test emitting the real [1013, "Outbound reply buffer overflow"] from an actual E2EEChannel — the production symptom, not a mock. Neutralizing the snapshot clamp fails its own oracle, re-proven after the test was relocated.

The second half of the report — never recovering without an app restart — is only partly addressed here and is now tracked as STA-3483: the browser stream restart arms a single 500ms retry and never reschedules, so any connection loss can strand the pane. Fixes STA-2970.
2026-08-05 01:26:38 -07:00
0f9caf52b1 fix(ssh): time out stalled remote file streams (#11364)
* fix(ssh): time out stalled remote file streams

* test(ssh): cover stream dispose-listener cleanup

* fix(ssh): pause file stream deadlines during sleep

* fix(ssh): replay suspended state to late streams

* fix(ssh): allow slower file stream progress

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 01:13:13 -07:00
Jinwoo Hong 950985645d fix(runtime): retire an exited pane's surface in its owning host partition (#12671)
`retireMobileSessionSurfacesForPty` called `getWorkspaceSession()` / `setWorkspaceSession()` with no host id, so every retirement wrote to the LOCAL partition — while its sibling `retirePersistedStablePaneOwner` correctly scopes to the SSH execution host.

For an SSH pane exiting cleanly this is not a harmless misdirected write. Measured on main: the write went to the local partition instead of `ssh:conn-1`; the SSH partition still held the dead PTY binding; the local partition gained a bogus topology revision for an SSH repo; and the published tab list contained a RESURRECTED leaf hydrated back from the stale SSH partition. The wrong-partition write was accepted — a tombstone recorded and the revision advanced for a surface that could not be found.

Found during independent review of #11542; pre-existing, not caused by it. Fixes STA-3463.
2026-08-05 00:51:42 -07:00
Jinwoo Hong 4e370062a8 fix(remote-runtime): make hidden-output recovery reason-driven instead of timer-guessed (#12655)
When a remote terminal tab is hidden, the host stops sending its output and discards what it queued, so on reveal the only way to recover the missed output is to ask the host to serialize its buffer. That reply was ambiguous — one empty answer covered several unrelated situations — so the client inferred "output is lost" from elapsed time, using budgets sized for local IPC. Over a network that guess was routinely wrong: users saw "[Orca skipped hidden terminal output because main recovery was unavailable.]" on a healthy pane and got a permanent scrollback gap, worst exactly when an agent was streaming heavily and there was the most to lose.

The key insight is that there is no provable-absence case at all. A pane with genuinely no retained output returns a SUCCESSFUL snapshot with empty data, because the host serialized fine and found nothing. The real defect was the host sending an untagged empty reply when no serializer answered — reporting an unprovable failure as proven emptiness.

The host now states why a snapshot is unavailable and the client acts on that reason: an empty snapshot is success; retry-worthy retries and then gives up honestly; permanently-unavailable banners immediately with no waiting; and a host too old to say latches that pane to the pre-existing timer heuristic. Local panes are unchanged. The self-heal repaint no longer yanks the viewport of a user scrolled back reading — it waits for the terminal to return to following output.

Retries are bounded by COUNTING REPORTED OUTCOMES, never elapsed time. Independent review found that the single budget also charged attempts for causes returned locally, where the host was never asked — meaning a re-arming resync could exhaust it and banner on a perfectly healthy host, a residual instance of this very bug. Host answers and local gates now have separate budgets; local gates send zero frames, so retrying them cannot pressure the host.

Review also found a duplicate-banner path where a repaint timer armed before a permanent answer survived the abandon; the clear is scoped to the branch that banners, since the retry loop deliberately arms that timer.

Wire change is additive: an optional field on an existing frame, dropped on the success path, so old clients see an unchanged frame. STA-3476 tracks replacing the legacy-host detection (currently inferred from an absent field) with a positive capability signal. Closes STA-3457.
2026-08-05 00:51:39 -07:00
Jinwoo Hong 15ef69a814 refactor(runtime): preserve transport error codes over IPC (#12667)
Errors thrown across Electron's `ipcMain.handle` lose their structured error code — only the message survives. So the renderer classified transport failures by matching substrings of English message text. That is how a queue-overload rejection escaped classification during a remote outage and surfaced as a raw error wall: the code was stripped in transit and its message fragment was not in the recoverable list.

This converts `RemoteRuntimeClientError` and `RuntimeRpcCallQueueOverloadError` rejections from `runtimeEnvironments:call` into the existing structured `{ok:false, error:{code,message}}` response, which the preload already passes through unchanged and `unwrapRuntimeRpcResult` already reconstructs with the code intact. Classification now treats a present code as authoritative and consults message fragments only when there is no code.

The fragment list is deliberately RETAINED as a backstop, not deleted: untyped main-handler rejections, subscription-start failures, and older code-less paths still rely on it.

Proven real rather than cosmetic: a test-only patch applied to unmodified main fails (4 failed / 66 passed) because the code does not survive the boundary today, and passes on this branch.

Independent review specifically chased the risk that a present-but-unrecognized code would now short-circuit to fatal where a message fragment previously rescued it — the shape that dead-ends a pane. It enumerated all 34 reachable code/message pairs and confirmed no pair flips recoverable to fatal, that the newly-serialized code set is closed and client-local, and that host-forwarded codes preserve recoverable classification by design. A differential harness over that corpus was verified non-vacuous by injecting the bad shape.

Nothing crosses the paired-runtime wire: desktop main -> IPC -> preload -> renderer only, reusing an existing response shape, no new fields or opcodes.

The connection-level offline state with a single reconnect affordance remains as STA-3456 follow-up work.
2026-08-05 00:44:05 -07:00
ShawnandBrennan Benson c4d5a535f2 fix(pty): restore macOS TCC attribution persistence (#12562)
* fix(pty): restore macOS login shell attribution

Replace the env intermediary with a clean bash trampoline that execs the configured shell as a login shell. Keep SHELL, custom arguments, fallback shells, and unsafe characters positional.

* chore(pty): log macOS TCC spawn strategy

Record wrapped versus direct daemon PTY strategy without private values so live TCC reports can distinguish launch behavior.

* fix(pty): harden macOS TCC login trampoline

* test(pty): update macOS TCC spawn expectation

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-05 00:36:22 -07:00
Jinwoo Hong 84df99e2f1 test(serve): pin zero duplicate agent resumes across headless serve desktop promotion (#12666)
STA-1716 reported that a packaged `orca serve` could become the single-instance owner after the desktop app exits, leaving Dock/Finder unable to restore a window — and that forcing a reopen made the headless process hydrate a renderer that interrupted and DUPLICATED live agent sessions.

Verification against main found every criterion already fixed (#8646 for desktop promotion and the fail-closed CLI, #12212 for duplicate serve activation, #12574 + #9729 for the resume/ownership guards). The genuine gap was criterion 6: the ticket's own automated regression never existed. An existing reliability gate asserted PTY identity survives promotion, but nothing asserted what the incident was actually about — how many agents the promoted renderer resumes.

This adds that coverage: a unit/service-level journey that drives the real single-instance lock, activation gate, settle and focus paths, then runs the real resume logic against a store seeded as a renderer freshly mounted inside the serve process, asserting zero duplicate resumes.

`settleServeDesktopActivation` moved from `index.ts` into its own module with identical semantics, so the test drives the real decision rather than re-implementing it — the earlier repro had to mirror that logic locally, which is the "test passes without running the scenario" failure mode.

Proven to be a real oracle: breaking each guard individually turns it red, and reverting the pre-#12574 pane form reproduces the incident exactly (two duplicate `codex resume` tabs).
2026-08-05 00:34:10 -07:00
e233d6a641 Recover automatically instead of getting stuck on "Multiplexer disposed" when an SSH relay drops (#12216)
* fix(ssh): recover instead of wedging when the relay channel dies mid-connect

Three coupled defects made a dropped SSH relay look like a permanent bug:

1. SshRelaySession.establish()/reconnect() ran their last liveness gate before
   configureRelayGraceTime(), whose mux.notify() can dispose the mux
   synchronously (writer control-lane admission cap, or a throwing transport).
   The session then latched _state='ready' + _onReady (status bar "connected")
   while watchMuxForRelayLoss() silently no-op'd on the dead mux, so the bounded
   relay backoff in ipc/ssh.ts never ran and the fs/pty/git providers stayed
   registered against a dead multiplexer. Both sites now re-check
   mux.isDisposed() after the notify and take the existing failure path.

2. SshChannelMultiplexer.request()/notifyWithSettlement() always reported the
   permanent-shutdown string 'Multiplexer disposed' with no code, even when the
   recorded dispose reason was connection_lost. The reason is now recorded and a
   shared disposedError() factory serves dispose(), request(),
   notifyWithSettlement(), so a transient drop reports
   'SSH connection lost, reconnecting...' / CONNECTION_LOST. onDispose() on an
   already-disposed mux now fires the handler synchronously with that reason
   instead of returning a silent no-op (without retaining it).

3. TerminalErrorToast no longer renders a transient relay drop in the red
   "please file an issue" style. The marker is matched with includes() because
   the message reaches the toast IPC-wrapped.

ssh-git-response-stream-reader registers its onDispose subscriber after the
abort wiring, since an already-dead mux now fails synchronously there and the
cleanup must be able to drop the caller's abort listener.

Closes #11953

* fix(ssh): treat a mux killed during PTY reattach as relay loss

reconnect()'s post-reattach gate bare-returned when ownsAttempt() went false,
and reattachKnownPtys swallows every per-PTY error, so a control-lane failure
during a large reattach burst disposed the mux without ever reaching the catch:
providers stayed bound to the dead mux, no relay-loss watcher was installed, and
the session wedged in 'reconnecting' until restart. Take the failure path when
our own mux is the one that died so ssh.ts's bounded backoff retries.

Co-authored-by: Orca <help@stably.ai>

* fix(ssh): recover when relay dies during setup instead of wedging

Introduce verifyRelayAttempt() to detect mux disposal at each setup phase
(consumer session, home resolution, provider registration, PTY reattach).
Routes mid-setup connection loss into relay-loss recovery instead of
hanging in reconnecting state.

* Extract SSH disposal error factory

Multiple sites were duplicating the disposal error creation logic with
specific message and code values. The renderer uses these to distinguish
temporary disconnects (show reconnection overlay) from permanent shutdown
(show error toast), so all producers must use the same factory to avoid
silent UI degradation.

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-05 00:13:41 -07:00
Jinwoo Hong 003114dfad fix(remote-runtime): stop the host tab mirror from fighting renderer-owned agent status (#12641)
A remote-paired terminal tab flickered several times per second between the agent-generated title with a running status, and the plain title "Terminal" with "Done - Claude" in the sidebar.

Two writers owned the same state. For remote panes the client parses agent status out of the terminal byte stream, while every host tab snapshot rebuilt the mirrored tab WITHOUT the client's generated title and re-decided status by comparing timestamps taken on two different machines. The host also treated a neutral live title ("Terminal") as proof the agent had finished, and re-stamped that conclusion with the pane's last-output time — so it advanced with every output byte and always looked newer. Neither writer could ever win.

This removes the second writer rather than trying to arbitrate two clocks: the client is authoritative for panes whose status it parses (only while attached, released on teardown), the host no longer invents a finished state from a neutral title, and the generated title is carried through snapshot rebuilds. The client was chosen as the authority because the host snapshot format carries no generated title at all — making the host authoritative would permanently lose generated titles on paired clients.

Purely local and plain SSH panes are structurally unaffected: they have only one writer.

Verified with a reproduction that is red on main (frames show done -> working -> done with the label flipping on every publication) and green with the fix. Independent review additionally found and fixed a defect where a superseded pane's late cleanup could permanently strip a live pane's authority, reinstating the very flap being fixed.

Deferred follow-up STA-3455: host `blocked`/interactive-prompt states can still pierce the fence and fall back to cross-machine timestamps; fixing that properly needs an origin marker on the status entry.
2026-08-04 23:21:34 -07:00
Jinwoo Hong 1f86f980c5 fix(ssh): let terminate reach remote PTYs the app gave up reattaching (#12642)
Every SSH reattach failure abandons the remote terminal without shutting it down, and abandoned terminals then became structurally unreachable — excluded from reattach enumeration and, critically, filtered out of the user-facing "Terminate sessions" action, so a user could not kill them even manually.

The core problem is a naming trap: `expired` never meant the remote shell died. It means the app gave up reattaching. It is written on reattach failure, on spawn-time expiry, and in bulk by a relay reset inside a `finally` that runs even when the force-stop threw. So the leases most likely to name a still-live orphan were exactly the ones the terminate path excluded.

This change is reachability only. Expired leases are now reachable by an explicit user-initiated terminate, with the relay's response used as evidence: a shutdown that reports the PTY gone tombstones the lease, and leases already proven terminated are left alone.

**No automatic kills were added.** Every abandon path was enumerated and none of them proves abandonment: attempts-exhausted knows nothing (the relay never answered), identity mismatch means a *live* PTY belongs to a different pane so killing it would destroy someone else's terminal, and not-found is the one branch with real proof of death — where the process is already gone and needs no shutdown. Per the rule that unprovable liveness never authorizes destroying a session, the abandon paths deliberately leave the process running.

Relay-side automatic collection of unattached PTYs is deliberately NOT implemented: the relay cannot distinguish an abandoned terminal from a deliberately detached one, and the unlimited default grace exists precisely so long-running work survives disconnects and host sleep. Any bounded reaper would be killing on absence of evidence.

Verified: 3 tests red on main. Negative tests assert each abandon path leaves the shell running and the lease terminable, proven real by mutation — adding a shutdown to the exhausted branch or expiring on identity mismatch each turns them red.

Fixes STA-3376.
2026-08-04 23:07:34 -07:00
Jinwoo Hong 9666087faf fix(ssh): preserve agent generation timeout budget (#12644)
AI commit-message and PR-field generation over SSH failed deterministically at exactly 30 seconds whenever the remote agent CLI took longer, reporting "Claude could not be reached on the remote PATH. Try again after the SSH connection recovers."

That message was wrong twice over: the SSH connection was healthy (terminals and git kept working on it) and the agent binary existed — it was simply still running.

Cause: the SSH channel multiplexer applies a 30s default deadline when a request omits its own, and the generation call passed none, even though the operation itself carries a 60s budget. The shorter transport deadline always won, and the resulting rejection was then mapped onto the generic connection/PATH error.

Fix: derive the transport deadline from the operation's own budget plus a margin at that call site (the global default is deliberately unchanged, since other callers depend on it), and classify a transport timeout as a timeout — reporting that the agent exceeded its budget and may still be running — while genuine connection and PATH failures keep their existing guidance.

Verified red on main first: a 45s response rejected by the 30s default, and a typed timeout mapped to the PATH message. Both green after. Caller audit covered commit messages, PR fields, branch naming and model discovery.

Fixes STA-3073.
2026-08-04 23:07:17 -07:00
PLANDER DEV 7 cbc005c8aa fix(remote-runtime): materialize the host surface when reconnecting a terminal pane (#11542)
Reconnect could never recover a terminal pane whose host-side process was gone (host restarted, or the workspace was never opened there): recovery only polled the tab inventory, which can never create the surface it is waiting for, so Reconnect spun for ~60s and gave up permanently.

Verified with a deterministic reproduction: on main the recovery path issues 51 inventory polls and zero activations across both an automatic online trigger and a manual Reconnect click; with this change the pane re-materializes, rebinds and accepts input.

Review found and fixed three further defects beyond the original change:
- an activation answered with a stale ready handle left the loop polling forever instead of re-activating;
- a non-missing activation failure (e.g. an older host without the method) never fell back to inventory;
- host-side, activating a parked surface permanently deleted the host tab, because an already-absent persisted binding was read as a competing owner *after* the destructive retirement had already run.

Independent review confirmed by mutation testing that every production change is covered by a test that fails when it is reverted, that only an authoritative inventory can retire a pane, that the loop is bounded under every failure mode, and that the unknown-liveness guard (proven death required before retirement) is intact.

Fixes STA-3002.
2026-08-04 22:59:06 -07:00
Jinjing eebaf47df0 Add 'Has Workspace' mode to show Linear issues linked to local worktrees (#12632)
* feat(linear): add 'Has Workspace' mode to show issues linked to local wo

Enable users to view and open existing workspaces attached to Linear issues
instead of accidentally starting duplicates. Includes shared worktree attachment
labeling for consistent UX across GitHub and Linear surfaces.

* fix(linear): apply search filter in 'in-orca' mode to prevent drops

- Apply search filter in 'in-orca' mode even without active context label to prevent
  team filters from silently hiding linked tickets (no "Fetch more" recovery path)
- Add aria-label to workspace-open button for accessibility
- Update tooltip from "local worktree" to "Orca workspace"
- Reorganize i18n: move workspace.open from lib.linear to components.issue
- Expand test coverage for workspace start and activation scenarios

* fix(linear): avoid mutating in-orca linked refs during render

React Doctor fails static analysis when refs are written during render.
Keep the latest linked refs in an effect so the in-orca loader can still
read them without re-running on identity-only worktree churn.
2026-08-04 22:12:00 -07:00
Brennan Benson c736031773 Fix setup-gated agent startup on long worktree paths (#12623)
* fix(worktrees): preserve gated agent startup on long paths

* fix(wsl): forward sequenced agent startup env
2026-08-04 22:01:35 -07:00
Brennan Benson 9507cbce0c fix(terminal): rate-cap WebGL atlas recovery (#12622) 2026-08-04 20:42:11 -07:00
Jinjing fe72eeb75c Add linked issue guidance and ELI5 sections to PR generation prompts (#12613)
* Add linked issue guidance and ELI5 sections to PR generation prompts

Include linked GitHub issues in PR descriptions with Fixes/Refs guidance, and require ELI5 Problem and Solution sections before implementation details. Tests verify linked issue substitution and prompt structure enforcement.

* Include linked issue details in PR description generation

- Fetch the linked GitHub/GitLab issue title and body so generated PRs reference real issue context instead of just a number
- Use provider-specific reference syntax (Fixes/Refs, Closes/Related to, AB#) and label the issue by the active provider
- Feed issue title and description into the generation prompt while treating them as untrusted context, never as instructions
- Fall back to a cached work-item title when the provider lookup fails, and skip cross-provider issue attachment
2026-08-04 20:05:30 -07:00
Brennan Benson 6919e326e6 Merge remote-tracking branch 'origin/main' into brennanb2025/agent-dashboard-ui-revamp 2026-08-04 19:37:44 -07:00
Neil 5f187e0836 fix(renderer): break Activity and terminal React #185 loops (#12600)
Breaks two independent React #185 (Maximum update depth exceeded) crash loops.

- Activity portal publication is idempotent by descriptor value, so a semantic
  no-op no longer bounces synchronously through Terminal and back into Activity.
- The Activity readiness burst budget survives slot, target, pane, and tab
  retargeting, and a quiet loading pane is rechecked when the window expires.
- Terminal cold-parking pins verdict bursts to the safe mounted side before
  React reaches its nested-update limit, with an expiry so tabs can park again.

Supersedes #12492 and #12485.

Portal descriptor equality is keyed off keyof ActivityTerminalPortalTarget so a
new field fails the build instead of silently suppressing a publish. Park-verdict
damping and breadcrumbs gate on pin liveness, and churn crumbs coalesce by
trigger so a burst cannot collapse into a slow-churn slot.
2026-08-04 19:07:47 -07:00
Brennan Benson 0ce108d935 fix(browser): add native-UA session profiles (#12608)
* fix(browser): add native-UA session profiles

* test(browser): add Google sign-in UA probe

* fix(browser): preserve native profile UA identity
2026-08-04 19:07:23 -07:00
Jinwoo HongandOrcaWin 5ed45739e9 fix(runtime): make sibling-workspace terminal-path resolution an explicit client opt-in (#12616)
files.resolveTerminalPath began returning a foreign worktree id + relativePath
for absolute paths owned by a sibling workspace, with no protocol or capability
gate. Mobile 0.0.36 in the field ignores resolved.worktree and reuses its own
worktree id for the follow-up files.open, so a tap on a sibling-worktree path
opened the WRONG worktree's copy of that file (on 1.4.168 the tap was a safe
no-op).

Gate the sibling-workspace lookup behind a new optional crossWorkspace request
field: clients that honor resolved.worktree opt in; everything else keeps the
pre-sibling-resolution contract. Old servers strip the unknown field (zod), so
every version pairing degrades to the safe legacy behavior. Optional-field
addition, so no RUNTIME_PROTOCOL_VERSION bump per protocol-version.ts rules.

The terminal-path RPC tests move to files-terminal-path-resolution.test.ts
because files.test.ts sits at the max-lines cap.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 18:46:36 -07:00
Brennan Benson 39c3c58d55 perf(runtime): gate terminal.list visual layouts (#12450)
* perf(runtime): gate terminal.list visual layouts and stop the false writable claim

visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out.

Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces.

* test(runtime): type the payload-size fixture arrays for tsc

* fix(runtime): preserve terminal list compatibility

* test(runtime): guard terminal list optimization

* fix(cli): preserve agent access to terminal layouts
2026-08-04 17:50:52 -07:00
Brennan Benson dc5c5a89ba fix(codex): block launches with unproven runtime auth (#12490)
* fix(codex): block launches with unproven runtime auth

* fix(codex): reconcile shared auth before resume
2026-08-04 17:38:48 -07:00
Jinwoo HongandOrcaWin 72245918a1 fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback (#12589)
* fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback

A daemon-backed terminal whose tab was never activated in the host UI was
never attached, so the daemon emitted no bytes: paired clients rendered
blank/frozen panes and `terminal read` returned an empty tail while the PTY
was alive.

- Runtime: first remote view subscriber of a known-but-unattached local
  daemon session triggers an attach through the pty controller — attach-only,
  no resize, no renderer mount/focus, headless-safe, deduped across
  concurrent subscribers, and never detached on release. Excludes SSH-scoped
  ids and sessions a local spawn already published this generation.
- Read path: withVisibleSnapshotFallback now falls back to the provider tail
  for an empty-tail never-attached live local session; unprovable state stays
  empty, never an error.
- pty controller: expose attach with getProviderForPty-style routing,
  answering false on doubt; local daemon provider only.
- Daemon adapter: attach rides the session's applied size instead of a
  hardcoded 80x24, sends attachOnly, and retires a pre-v31 daemon's
  accidental spawn instead of publishing it.

Deterministic harness drives the real terminal.multiplex handler against a
real OrcaRuntimeService with an injected daemon-model controller whose data
events are gated on attach; covers snapshot-capable and snapshot-null
daemons, concurrency, release, replacement-spawn exclusion, and negative
safety. Red on base, green with the fix, red again with the fix reverted.

* fix(terminal): refuse degraded-provider attach fallback and surface failed legacy-spawn retire

Verifier follow-ups on subscriber-driven daemon attach:

- DegradedDaemonPtyProvider.attach routed unknown ids to the in-process
  fallback, whose no-op attach resolves — the runtime then pinned a
  subscriber-driven attach as succeeded while the stream stayed blank.
  Attach now refuses any route that resolves to the fallback (a fallback pty
  cannot own a daemon-surviving session), so the controller answers false,
  no sticky success is recorded, and a later subscriber attaches once a
  daemon adapter proves the id. Session-probe adoption moved to
  degraded-daemon-session-routing alongside the new refusal.
- The pre-v31 attach-only TOCTOU retire (accidental legacy spawn kill) now
  logs a warning with the sessionId on kill failure instead of swallowing
  it, so an orphaned replacement shell is diagnosable.

Regressions: degraded provider refuses unowned/fallback-owned attach and
routes to a daemon once it proves the id (red on previous commit); runtime
harness pins refused-attach retry for a later subscriber; adapter test pins
the surfaced kill failure.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:25:20 -07:00
Jinwoo HongandOrcaWin 51ca82d028 fix(runtime): seed terminal previews and titles from restore payloads (#12579)
* fix(terminal): seed list/read records from reattach restore payloads

After an app relaunch the PTY daemon survives and spawn silently
reattaches, but the restore payload (reattach snapshot, cold-restore
scrollback, relay replay, lastTitle) arrives as a spawn RPC result and
never passes through runtime.onPtyData — the only feeder of the terminal
records behind `terminal list`/`terminal read`. Every restart therefore
left connected terminals with empty title/preview/lastOutputAt and a
zero-line read tail, blinding orchestrators that poll terminals.

The spawn flow now calls runtime.seedTerminalRestoreTail with the restore
text and lastTitle, unconditionally of the renderer-authority emulator
gate (the records are main-side only). The seed reuses the live path's
normalize/tail/preview pipeline on a capped 256 KiB suffix (re-anchored
at a line boundary so a cut escape cannot leak), only fills records that
never saw output (a remount reattach cannot re-apply history), routes
titles through the applySeededAgentStatus precedent (state writes only —
no waiters, no side-effect facts), and never stamps lastOutputAt or
waitBlockedAt: restored bytes are historical, not fresh activity.

lastTitle is threaded from the daemon reattach snapshot and cold-restore
checkpoint into PtySpawnResult; relay replays seed preview only. SSH and
runtime-controller paths are unchanged — seeding is gated on the fields
existing.

* fix(terminal): seed restore records on the controller spawn path and prime the wait baseline

Follow-ups to the restore-record seed, from independent verification:

1. The runtime-controller spawn flow (createTerminal background creates —
   headless `orca serve`/CLI — and pane splits) never consumed restore
   payloads, so the exact orchestrator-blindness this fix targets survived
   on the topology that needs it most. The extraction now lives in one
   helper called from both spawn choke points (renderer pty:spawn and the
   controller flow); the runtime's empty-record guard makes overlapping
   seeds a no-op.

2. The throttled per-PTY wait scanner starts with a null baseline, so a
   permission prompt visible only in seeded HISTORY read as newly gained
   on the first benign live chunk and stamped waitBlockedAt "now".
   Seeding now primes the scanner baseline from the seeded tail without
   stamping; only a signal appearing in genuinely new output counts.

3. Cap re-anchoring accepts \r as well as \n (newline-free CR-redraw
   streams), consuming a full \r\n pair so the seed does not start with
   a phantom blank line.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:23:01 -07:00
Jinwoo HongandOrcaWin 3d8131d7ea fix(runtime): reject leaf terminal sends only on controller-proven PTY absence (#12578)
* fix(runtime): reject leaf terminal sends only on controller-proven PTY absence

orca terminal send to a leaf whose ptyId no provider in this process owns
was a silent no-op reported as success: the graph mirror answers
writable=true, every provider write to an unknown id is accepted
fire-and-forget, and bytesWritten is computed from the payload rather than
delivery. sendTerminal and sendTerminalAgentPrompt now consult a controller
liveness probe when the provider does not synchronously know the id
(hasPty), and throw terminal_not_writable only on an exact false — unknown
liveness, probe errors, SSH/remote scopes, and probe-less providers never
reject (#12393's rule: null is not absence), so a restored daemon session
still accepts writes before its pane remounts. Push-on-idle orchestration
delivery gains the same gate so a proven-dead leaf keeps its messages
queued instead of marking them delivered into a void.

The pty controller now exposes probePtyLiveness, routed like write: a
provider probe is preferred, the in-process local provider's refusal is
authoritative (sole owner), and remote-scoped or SSH ids without a probe
answer null after awaiting the cold-start daemon swap. Proven-absent
verdicts cache 15s per ptyId with in-flight dedupe, superseded the moment
the provider re-learns the id.

* fix(runtime): arm one probe-deferred delivery continuation per pty

Review (GPT verifier) confirmed: triggers arriving during one in-flight
absence probe each attached a continuation to the deduped probe promise, and
since Claude-target delivered_at stamps only after the delayed Enter, every
continuation re-read the same unread rows — double payload injection and two
armed Enters. Single-flight the deferred continuation per pty; the one armed
continuation re-reads fresh rows when it fires, so nothing is lost, and the
guard clears on settle so later triggers defer again. The narrower
pre-existing 500ms sync-path window is unchanged and out of scope.

* fix(runtime): single-flight the whole orchestration delivery window per pty

The probe-continuation guard cleared at probe settle, but Claude-target
delivered_at stamps only in the delayed-Enter callback ~500ms later — a
trigger landing in that gap armed a fresh probe cycle, re-read the same
un-stamped rows, and re-injected the payload. The identical window existed
on the pure sync path pre-PR (two triggers within 500ms double-deliver).

Hold a per-pty delivery-in-flight flag from before the payload write until
delivery settles: entry-checked before reading unread rows, cleared through
one settle point covering the failed write, the sync-stamped coordinator and
Cursor branches, any sync throw, and the delayed-Enter callback on submit,
refusal, and throw alike. A trigger arriving mid-flight is not dropped — it
parks the latest leaf per ptyId and re-runs delivery once on settle, so rows
inserted mid-flight deliver without waiting for the next idle event. The
probe single-flight stays; the new guard subsumes its post-settle gap, and
no trigger site bypasses it.

Both strengthened tests are red on the previous commit (first subject
injected twice) and green here: in-window re-trigger on the probe path and
sync-path double-trigger each deliver the first batch exactly once, with the
parked second row delivering alone after settle.

* fix(runtime): retire the armed delivery Enter on pty exit; guard fire-time on current state

Two variants of one root cause — the delayed-Enter callback outliving the
session it was armed for:

1. Cold restore respawns under the same session id. onPtyExit never
   cancelled the armed Enter or the in-flight delivery state, and
   onPtySpawned flips the same leaf writable again — so an exit + same-id
   respawn inside the 500ms window let the stale callback inject \r into
   the replacement session and stamp rows it never received, then settle
   against a newer same-id flight.
2. Graph resync replaces leaf objects, so onPtyExit flips writable=false
   only on the current replacement; a callback trusting its closed-over
   snapshot still read writable=true and fired after exit with no respawn.

The flight record now carries its armed Enter timer and serves as settle
identity: onPtyExit clears the timer and drops the flight and any parked
re-delivery without stamping (rows stay unstamped and re-deliver on the
replacement's next idle — the existing contract), and settle no-ops unless
its own flight is still current, so a stale settle can never clear a newer
same-id flight or flush its parked trigger. At fire time the callback
re-resolves the leaf by key and requires the same ptyId binding and current
writability instead of reading the closure snapshot.

All three regressions are red on the previous commit: same-id respawn saw
\r plus a false delivered_at stamp, exit leaked the flight and parked
state, and the orphaned-snapshot resync variant fired Enter after exit.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:22:10 -07:00
Jinwoo HongandOrcaWin 27da04d50d fix(terminal): truthful handle liveness + no forked resume tabs for hidden restorable panes (#12574)
* fix(runtime): report terminal handles disconnected on controller-proven PTY absence

leaf.connected mirrors the renderer graph (ptyId !== null), so a restored
surface whose PTY died with a prior process was listed connected/writable
forever with empty title/lastOutputAt/preview — the exact signature automation
saw on run6 workspaces after a restart. listTerminals now threads the
controller inventory it already fetches into buildTerminalSummary and demotes
only on proven absence, only for locally-scoped ids; unknown liveness and
SSH/remote scopes never demote, and no session or pane is retired.

* fix(terminal): stop forking hidden restorable panes into replacement resume tabs

paneWillConnectOnActivation still assumed the pre-keep-alive mount model, but
every non-parked tab of the active worktree mounts and connects hidden at 0x0.
Activation therefore appended a replacement resume tab per non-group-active
agent pane and handed it the sleeping record, stranding the hidden pane as a
bare shell — or forking two live surfaces onto one provider session when the
old PTY survived in the daemon. The predicate now answers "will mount and
connect": any non-web-mirror tab of the active worktree qualifies; non-active
worktrees still answer false so background wake keeps its append-based resume.

Contract change: reverses the hidden-tab expectation from #6800, whose premise
(hidden panes never connect) no longer holds; that test is updated in place.

* test(terminal): pin the remote-scope exemption and the web-mirror ownership exception

CodeRabbit flagged both exclusions as untested: a remote-runtime-scoped leaf
absent from the local inventory must stay connected (its inventory lives on
the remote host), and a web-mirror tab must not own sleeping-session recovery
(it never mounts a local pane), so the appended replacement remains its
correct resume path.

* fix(terminal): rescue just-spawned ptys from absence demotion; unpark panes owning sleeping records

Review (GPT verifier) confirmed two gaps:
- listTerminals demoted a live just-spawned PTY when listProcesses snapshotted
  before session registration (the sweep's hasPty rescue is leaf-gated), and
  federation reads one connected:false as exited. The summary's proven-absence
  check now also consults the provider's sync hasPty.
- Ordinary per-tab cold parking (30s hidden) kept a non-group-active pane
  unmounted, so a sleeping record it owns under the new ownership predicate
  could not cold-restore until the user revealed the tab. Per-tab parks now
  exempt panes owning a sleeping-session record; worktree-level parks are
  untouched (they clear on activation).

* fix(terminal): reconcile the daemon session cache on inventory; scope the park exemption to consumable records

Round-2 review confirmed two holes in the round-1 fixes:
- DaemonPtyAdapter.hasPty is cached activeSessionIds membership, and a
  successful listSessions never removed ids the authoritative inventory
  omitted — an exit missed while the socket was down kept hasPty true
  forever, and the new spawn/list-race rescue would trust it, reopening
  connected-forever for that pty. listProcesses now drops pre-request cached
  ids the inventory does not list alive (ids spawned mid-flight are snapshot-
  protected).
- The park exemption covered records a pane can never consume
  (automaticResumeBlockedBy, passive-completed evidence), pinning hidden
  panes mounted indefinitely. The exemption now lives in
  sleeping-record-park-exemption.ts and requires a consumable record.

Also pins the web-mirror replacement's resume claim and startup command
(CodeRabbit round-2).

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:20:09 -07:00
NeilandOrca 7956335cea fix(setup): stop caching an unreadable orca.yaml as "no setup script" (#12469)
* fix(setup): stop caching an unreadable orca.yaml as "no setup script"

`checkRepoHooks` returned `{hasHooks:false, hooks:null, mayNeedUpdate:false}` with no `status` field when the SSH filesystem provider was unavailable, and inside a blanket catch for any read error. The renderer only bails on `status === 'error'`, so that status-less false negative was cached as an authoritative "no setup script" and the prompt stayed on screen.

Mirror the `hooks:check` IPC twin exactly: `status:'error'` for a missing provider, ENOENT-aware in the catch, `status:'ok'` on the folder-repo, binary, SSH-success and local branches.

Fixes #8752

Co-authored-by: Orca <help@stably.ai>

* test(e2e): add recordable proof for setup-script-prompt-false-negative

Fails on origin/main, passes on this branch.

Test: recovers from an unreadable orca.yaml instead of pinning the failed verdict

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 17:06:50 -07:00
Brennan Benson 8c65dd5094 perf(runtime): keep PowerShell ACL work and a second auth off the remote command path (#12451)
* perf(runtime): keep PowerShell ACL work and a second auth off the remote command path

Two costs sat on the remote authentication path on Windows:

- The E2EE handshake persisted `lastSeenAt` inline, and every secure-file write
  spawns PowerShell synchronously twice to reapply the registry ACL, so the
  client's `e2ee_authenticated` waited on both spawns.
- Every remote CLI command except `status.get` opened a second full WebSocket
  connection just to re-read status for the protocol-compat check, doubling the
  authentications per command.

The first sighting of a device still persists inline (rotation drops entries
disk says were never scanned); later refreshes update memory now and coalesce
onto one deferred write. The compat verdict is saved against the runtime's
per-launch `runtimeId`, so a restarted or upgraded runtime retires it.

* fix(runtime): preserve compatibility on one remote auth

* fix(runtime): flush registry after transport shutdown
2026-08-04 17:04:51 -07:00
Brennan Benson 9deee5ad2f perf(worktrees): delete worktree directories after the removal returns (#12416)
* perf(worktrees): delete worktree directories after the removal returns

`git worktree remove` deleted the whole checkout inline, so the remove IPC held the
watcher/PTY gate for the entire recursive delete (prod traces: worktree.remove.git_remove
p50 8-14s, p90 29s, max 34.7s). Local removals now rename the checkout into a hidden
sibling trash root, clear Git's registration for the missing path, and delete the moved
tree in the background. Renames that cannot run (WSL, other volume, Windows open handles)
fall back to the previous in-place removal unchanged.

* test(worktrees): keep no empty trash root when the rename cannot run

* fix(worktrees): harden deferred trash cleanup

* fix(worktrees): keep WSL trash on its owning host
2026-08-04 15:53:11 -07:00
Brennan Benson 40ea4ece1a Track Claude models from the installed CLI per host (STA-3330) (#12369)
* feat(native-chat): track Claude models from the installed CLI per host (STA-3330)

The Claude seed no longer pins version labels to aliases that resolve
differently across CLI versions, and the catalog now defines listModels
backed by a one-shot list_models control request over --print stream-json.
Hosts whose CLI predates the request answer with a control error and keep
the seed. Discovery also feeds Source Control AI via the commit-message
spec, and the /model echo detector matches resolved model names.

* fix(native-chat): preserve discovered Claude capabilities

* fix(native-chat): tolerate malformed Claude model entries

* fix(native-chat): discover models in folder workspaces

* fix(native-chat): trust discovered Claude capabilities

* fix(native-chat): remove Claude model fallbacks

* fix(native-chat): keep the Claude model picker rendered

The Claude picker rendered nothing until the per-host `list_models` probe
returned, so it popped in ~1s after mount and never appeared at all when
the probe failed — an old CLI without `list_models`, no `claude` on PATH,
or an older remote runtime whose response omits `catalogOrigin`.

Restore the version-neutral family seed as the starting list; discovery
still replaces it wholesale on success, so a host with a real catalog
never shows an obsolete hardcoded row.

Separately, the tracked model could fall outside the active list: the
terminal header scrape yields family ids (`opus`) while a current CLI
lists `opus[1m]` and no plain `opus`. That blanked the picker trigger and
dropped the model's effort and fast-mode controls. Reconcile the tracked
id into the active list once, so the snapshot, the appliers, and typed
command recording all see a labelled, operable row for it.
2026-08-04 15:47:29 -07:00
847c8c852d fix(agent-status): correlate manual Claude compact hooks (#12332)
Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 15:03:57 -07:00
Jinjing 2073f7eeb2 fix(startup): omit Linux-only package mgrs from PATH on non-Linux (#12566)
Snap and Linuxbrew don't ship installers for Darwin or BSD, so seeding
their PATH entries on those platforms adds phantom directories every
spawn must stat. Keep them on Linux only, while preserving Nix and
Homebrew across all platforms as they have multi-platform support.
2026-08-04 13:45:50 -07:00
Brennan Benson 640bd81c39 feat(agent-map): restore the agent launcher and declutter map labels
Three gaps in the experimental Agent Map:

- The "start a new agent" picker was split onto a preserved branch during the
  08-02 rebase (47829cb226) and never re-landed. Restores that commit and its
  pop-out IPC, keyed on the raw worktree id rather than the map identity.
- Workspace labels draw at a fixed screen size with no collision handling, so a
  zoomed-out map stacked dozens of names on each other. Adds a declutter pass
  that seats project names first, then workspace names by attention, then
  project counts in whatever room is left.
- The pop-out had no workspace right-click at all: its renderer has no store, so
  the shared sidebar menu cannot mount there. Adds a snapshot-driven menu with
  the launcher and Sleep, relayed to the main renderer.
2026-08-04 12:35:57 -07:00
Brennan Benson 763708cece Merge main into brennanb2025/agent-dashboard-ui-revamp
# Conflicts:
#	src/renderer/src/i18n/locales/zh.json
2026-08-04 11:41:40 -07:00
2548b816c0 Keep the app responsive when security software slows process creation (#12217)
* fix(ports): keep the app responsive when security software slows process creation

Orca ran the workspace port scan's probe commands (lsof/ps on macOS,
netstat + powershell.exe on Windows) directly in the Electron main process.
libuv performs process creation inline on the calling event loop, which in
the main process is the browser UI thread, so an endpoint-security module
hooking CreateProcessW froze the whole window for the length of the spawn.

The same stall also produced a false diagnosis: the 4s command watchdog was
armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its
deadline had already passed by the time the command started. Every scan on a
hooked host reported a command timeout, tripping the 60s -> 5min backoff and
the "Port scanning is temporarily paused after a command timeout" banner even
though the commands themselves were healthy.

Probe commands now run on a lazily created, unref'd worker thread with FIFO
one-at-a-time dispatch, and the watchdog is armed after execFile returns so it
measures the command rather than the spawn. Node's own execFile timeout kill
(killed: true) is classified as a command timeout, keeping the backoff working
for genuine hangs. A scan that observes a stalled spawn skips its optional
metadata commands for that cycle, capping a hooked-host scan at roughly one
stall instead of three.

Closes #11161

* fix(ports): keep advertised URLs when a stalled spawn skips port metadata

Review follow-up on #11161. The stalled-spawn early return handed
scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed
attribution and reconcileAdvertisedUrls told the watcher each worktree's
listeners had vanished. shouldEvictAfterScan then deleted every cached
advertised URL and broadcast a removal event; those URLs are only ever
captured from live PTY output, so the dev-server link was gone until the
server restarted.

The scanners now report metadataAvailable, and reconciliation is skipped for
a scan that never gathered attribution evidence. The skip is also no longer
self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely
on the current scan's spawnMs made every port permanently external (Stop
refused with 'Only workspace-owned local processes can be stopped here.').
Metadata is now re-probed on the scan after a skip, matching what the comment
and test name already claimed.

Co-authored-by: Orca <help@stably.ai>

* test(windows): stop a temp-dir lock from failing the CLI launcher smoke test

The native launcher assertions passed on windows-latest, but teardown's
rmSync raced Windows' release of the image handle on the exe the test had
just executed and threw EPERM, failing the job.

Cleanup now retries and, on Windows only, tolerates a residual lock code
instead of reporting it as a launcher regression.

Co-authored-by: Orca <help@stably.ai>

* fix(ports): scope the metadata skip away from attribution-dependent scans

The metadata skip was a process-wide parity flag, so Stop and the
localhost-label allowlist could land on a degraded cycle and reject a
port the panel had just shown as workspace-owned. Give those callers an
explicit requireMetadata option, and carry the previous cycle's listener
metadata forward so a skipped background scan no longer republishes
workspace ports as external.

Also pin the watchdog ordering: the stall in the execution test was
shorter than the watchdog budget, so a watchdog armed before execFile
still passed.

* build: guard worker-thread entries against electron imports (#11161)

Electron's module is not registered on worker threads, so
require("electron") throws "Cannot find module 'electron'" inside a
main-process worker and kills it at startup (verified on Electron 43.1.0).
plain-node-entry-guard covered only forked plain-Node entries, so the five
worker entries relied on hand-written "must stay electron-free" comments.

The port-scan probe worker is one import away from
port-scan-command-client.ts, which deliberately contains require('electron').
A violation there fails closed at runtime while every unit test still passes,
because the client's require is try/caught on the main thread.

Covers stt-worker, warp-theme-parser-worker,
session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry
and port-scan-command-worker-entry. The scan is transitive over the emitted
chunk graph, so a shared chunk that reaches electron is caught too.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>

* test(windows): retry teardown for main's duplicate-PATH launcher fixture

Main's new csc-compiled harness runs an exe from the temp tree, which is
exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists
for; its bare rmSync would report a teardown lock as a launcher failure.

Co-authored-by: Orca <help@stably.ai>

* test(ports): pin the packaged-asar worker entry path

resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 02:03:40 -07:00
Neil e1071f59e9 Say why an update install failed instead of stalling for three minutes (#12224)
Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC.

Relates to #11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open.
2026-08-04 02:02:53 -07:00