Commit Graph
6229 Commits
Author SHA1 Message Date
Brennan Benson 44ec8779c0 fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494)
Azure DevOps Server rejects api-version=7.1 with 400
VssInvalidPreviewVersionException unless the -preview suffix is supplied,
so auth and every Git endpoint failed. Retry once with -preview on that
rejection and remember the requirement per origin. Also stop letting a
same-origin ORCA_AZURE_DEVOPS_API_BASE_URL (collection-level, needed only
for the connectionData auth probe) override the project-level base derived
from the remote for Git endpoints; cross-origin (proxy) overrides keep
working.
2026-08-05 18:52:25 -07:00
Brennan Benson 79896cb9a6 fix(native chat): retain transcript while reconnecting (STA-3333) (#12495)
* fix(mobile): keep the cached transcript visible while reconnecting

A manual retry closes the client and opens a fresh one, so the chat session
hook saw a new client under an unchanged identity, dropped its settled read,
and handed out an empty list — the transcript collapsed to a full-screen
spinner until the swapped client's snapshot landed.

Hold the last settled list per identity (captured post-commit) and keep
rendering it while the re-read is in flight. `transcriptLoading` still gates
consumers that decide from an empty transcript, so the launch-draft seed is
unaffected. The held list is keyed by a new `sourceIdentity` (host/workspace)
in addition to agent/session/transcript, so it can never serve another
source's messages.

Refs STA-3333.

* test(mobile): assert the whole reconnect window, not just its first frame

The re-subscribe lands a commit after the first render of the swap, so a
regression that cleared the held list there left frame 0 green and still
blanked the transcript. Verified: clearing the cache in the subscribe
cleanup now fails this test, where before only the view-toggle test caught it.

* fix(mobile): don't derive a tappable ask card from the held transcript

The cache this PR adds keeps the previous list rendered while a swapped
client re-reads. useMobileNativeChatPrompts was the one consumer reading
`messages` without honouring `transcriptLoading`, so an ask answered on
the terminal resurrected as a live, tappable card during that window.

Gating on `transcriptLoading` is exactly base behaviour: `setRead` only
ever stores 'ready'/'error', so status==='loading' implied an empty list
before this PR. The live `askFromStatus` path is untouched.

* chore: keep merge formatting scoped
2026-08-05 18:06:36 -07:00
Jinwoo HongandJinwoo-H aa64ac9606 fix(runtime): degrade focus-requested terminal create on headless serve (#12791)
`orca serve` publishes a ready graph under HEADLESS_RUNTIME_WINDOW_ID with no
BrowserWindow behind it. `shouldCreateInBackground` only degraded when the
create was renderer-backed, so any focus-requested create fell through to
getAuthoritativeWindow() and threw "No renderer window available" — leaving
`terminal create --focus` with no workaround on a remote server (#10333).

With a worktree selector and no renderer window, a background spawn is the only
usable path, so collapse the renderer-backed window check into a plain
"no window" check. That is the existing rendererBacked clause plus exactly the
missing focus case, and it drops the confusing `rendererWindow === null`
indirection (rendererWindow is already gated on rendererBacked).

Focus is not lost by the degrade: the spawned pane is still published to the
session-tab model and revealed with `activate: true`, which is how a paired
client learns about it. Mirrors the in-tree precedent in
runCreateMobileSessionTerminal.

Headed hosts are unaffected — the clause only fires when no window exists.

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 17:49:04 -07:00
Brennan Benson 6df8997c3c fix(file-explorer): sort numbered file names naturally across every listing surface (#11576)
* fix(file-explorer): sort numbered file names naturally

The File Explorer compared names with bare localeCompare, so numbered
files listed 100, 200 before 99. Hoist the numeric collator Source
Control file rows already use (#10850) into src/shared and apply it to
the local and runtime directory listings, the name-filtered view, and
Source Control directory nodes, which were inconsistent with the file
rows one line below (#11426).

* fix(file-explorer): natural sort on SSH funnels, relay, and pickers

Adversarial-review round 1 rework:
- Both readDir funnels short-circuited to the SSH filesystem provider
  before the patched sort, so SSH workspaces kept lexicographic order;
  re-sort locally after the provider returns (the remote relay may be an
  older build), and fix the relay's own comparator for relay-native
  consumers.
- sortDirEntries (shared, unit-tested) owns the directories-first +
  natural-order listing contract used by every funnel.
- compareFileNames breaks numeric-collation ties ('2' vs '02') by code
  units so sibling order stays total instead of readdir order, and pins
  the collator locale to 'en' so every host produces one order.
- The SSH folder browser and runtime server dir picker now match the
  Explorer they browse into.
- Ordering pinned by tests at the relay, source-control tree, and shared
  helper.

* fix(mobile): natural sort in the mobile file explorer

Mobile re-sorted host readDir results with bare localeCompare, undoing
the host funnel's natural order (round-2 review). Reuse the shared
comparator and pin the order in the mobile suite.

* fix(file-explorer): natural sort at the renderer choke point and remaining ties

Round-3 review: the remote-runtime RPC and paired-web routes return the
host's order verbatim, so re-sort in readFileExplorerDirectory where
every desktop route converges; pin the SSH funnel with a handler-level
test; and route Source Control path compares through compareFileNames so
numeric-collation ties share one total order with the Explorer.

* docs(file-name-sort): state the real perf baseline in the hoist comment

* refactor(source-control): drop the dead collator export; pin the test oracle locale

* fix(file-listings): cover remaining natural-sort surfaces
2026-08-05 17:11:28 -07:00
Jinjing 9ce9ce9a2e Register PTY output processors in memory profile census (#12795)
Add a new census module that tracks pending and retained OSC sequences
across all active PTY output processors. Each processor registers a gauge
at creation and unregisters it on dispose, detach, or destroy — this
prevents retained gauges from inflating later heap high-water profiles
and allows the memory profiler to detect stalled processors as a sign of
leaks.
2026-08-05 17:04:22 -07:00
Brennan Benson 4774d38239 fix(sidebar): keep PR status visible with activity (#12793) 2026-08-05 16:58:55 -07:00
Jinjing 2ff2a1b268 Display SSH worktrees immediately using persisted metadata (#12646)
* Display SSH worktrees immediately using persisted metadata

Users can now see known worktrees for SSH hosts without waiting for the
provider connection to establish. Worktrees are fetched from local metadata
and displayed as non-authoritative, then merged without replacing richer
live data once the provider becomes available.

* Show SSH folder workspaces immediately via persisted metadata

Add safeguards for metadata fallback: track authoritatively removed
worktrees per host to prevent resurrection, position new rows within
the host block to avoid jumping on authoritative scan arrival, and
preserve co-owner detection status during merge. Coalesce concurrent
metadata fetches to dedupe overlapping queries.
2026-08-05 16:52:49 -07:00
Jinjing ae1ed5e886 Remove source control group order preference (#12785)
* Reorder source control to show staged changes first by default

Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.

* Drop source control group order user preference

Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.

* Reorder source control to show changes before staged

Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
2026-08-05 15:29:46 -07:00
Jinjing 2c2a3266a6 Change question card submit button label to 'Submit' (#12782)
- Replace 'Send answer' with 'Submit' for clarity and consistency
- Update all locale translations (en, es, ja, ko, zh)
- Remove fixed button width and add whitespace-nowrap for flexible sizing
- Update component and test references
2026-08-05 14:58:17 -07:00
Jinjing debf4affe7 Display total lines of code change in branch header (#12771)
* Add branch line total chip to source control header

Display the total lines added and removed across a branch from its fork point, measured via `git diff <mergeBase>`. Only computed when the chip is visible (request gate on merge base OID), with 500ms soft deadline to protect status latency and 15s hard timeout. Deduplicated across concurrent pollers and cached alongside line stats. Omitted on failure — always shows exact or nothing, never a partial estimate. Updates throughout the stack: native git status, relay, renderer store/API, and UI components.

* Pin branch line total to app locale

Format line counts using the app's configured locale instead of the system
locale, ensuring consistent cross-platform display and test reliability.

* test: wait for coalescer joins instead of fixed sleep

Hold the diff until the second status pass actually takes the
branch-total coalescer lease instead of using a fixed 400ms sleep.
Fixes timing-dependent flakiness on slow machines.
2026-08-05 14:46:00 -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 d4dfc35ac4 fix(mobile): preserve multi-image chat attachments (#12639)
* fix(mobile): preserve multi-image chat attachments

* fix(mobile): use preferred array syntax

* fix(mobile): harden multi-image attachment flow

* fix(mobile): retain first-send image previews
2026-08-05 13:23:29 -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
Maxon PhongandJinjing b1b291db08 feat(browser): add hard reload option and shortcut hints to reload button (#12483)
* feat(browser): add hard reload option and shortcut hints to reload button

Add a tooltip to the browser reload button showing the reload shortcut.
Add a right-click context menu with Reload and Hard Reload options.
Add localized labels for Hard Reload across EN, ZH, JA, KO, ES.

* fix(browser): add aria-labels to reload buttons

* feat(browser): make reload button contextual and extract action logic

- Button label now reflects actual action: Stop when loading, Retry on failure, Reload when idle
- Extract reload intent resolution into reusable browser-reload-action module with tests
- Add keyboard support (Enter/Space) for the reload button
- Simplify remote page reload to tooltip-only (no ignore-cache RPC for remote pages)
- Add "Stop" translations for all supported languages

* fix(browser): exhaust reload intent switch for type-aware lint

Replace the default branch with an explicit reload case so oxlint
switch-exhaustiveness-check accepts BrowserReloadIntent.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-05 12:25:21 -07:00
Jinjing d021e1b711 Improve gh issue table (#9068)
* Refactor GitHub work-item mutations onto a shared optimistic coordinator

- Extract PR/issue status, assignee, reviewer, and merge/auto-merge mutations
  out of TaskPage cell components into a registry-backed
  begin/confirm/rollback pipeline (task-page-github-work-item-mutation-*),
  so soft-hide, sticky filter-membership, and quiet revalidation behave
  consistently across all mutation types instead of each cell re-implementing
  optimistic update/rollback/toast logic.
- Add quiet revalidation (no filter skeleton, no page blanking) and soft-hide
  handling so a row that exits the active filter (e.g. closing an issue under
  `is:open`) stays hidden without a jarring list reflow.
- Restyle the GitHub task table: opaque sticky ID/Title cells, distinct header
  fill, accent hover, and tighter row/toolbar chrome to fix background bleed
  and muddy contrast in the scrolled table.

* Fix quiet-revalidate cancellation and sticky-hide scoping in TaskPage

- Replace per-render `cancelled` flag with a ref that only flips on
  true unmount, so a nonce-triggered re-render no longer strands the
  shared quietState's trailing/backoff bookkeeping mid-flight.
- Fix backoff index to use max lag attempts instead of lagging-key
  count, matching processTaskPageQuietRevalidateSettle so several
  single-lag items can't jump the delay tier.
- Scope sticky-hide retention in materializeTaskPageItemList to the
  originating query key, preventing non-membership confirms (e.g.
  auto-merge) from lingering as stale rows across refetches.

* Fix is:draft filter to soft-hide non-draft PRs

Previously state was forced to 'open' for is:draft queries, so a PR
that stopped being a draft still passed the state check and stayed
visible. Add an explicit draft check to soft-hide it.

* Improve GitHub work-item mutations with scoped quiet revalidation

Prevent race conditions and stale data by tracking quiet run ownership,
validating scope changes with generations, and blocking overlapping mutations
with pre-flight checks. Extract quiet state management into a dedicated module
with improved authority clearing and network retry logic.
2026-08-05 11:48:50 -07:00
Jinjing 6c2d168f74 refactor(sidebar): restructure filters into consistent submenu layout (#12759)
Reorganize the host and project filters to share a unified single-row design
(label left, value right) with detailed selection moved to nested panels. Group
both filters under a "Show" section label to keep the parent menu flat. Extract
project-filter search logic into SidebarProjectFilterPanel with explicit focus
and keyboard-handling tests.
2026-08-05 11:36:14 -07:00
Brennan Benson 86b878cfd6 fix(mobile): parse classified PR lookup outcomes (#12659) 2026-08-05 11:31:11 -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 7aba9b306b Validate auto-generated PR details; abort with clear errors (#12752)
* Validate auto-generated PR details; abort with clear errors

Add fail-closed validation for generated review fields to ensure empty
bodies and generation failures prevent unintended auto-submission, with
user-visible error messages. Preserves current base ref since intent
auto-submits without confirmation.

* Add translations for PR generation error messages
2026-08-05 11:09:46 -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
Jinjing ade4354ddd Open GitHub items in workspace composer with issue automation (#12653)
* Open TaskPage GitHub items directly in workspace composer

Remove background creation indirection. The composer prefills with
issue/pull-request metadata from the GitHub work item.

* Generalize workspace composer tests to support multiple sources

Rename test from task-page-github-composer-boundary to task-page-workspace-composer-boundary. Add test verifying Linear items open directly in the workspace composer, expanding beyond GitHub-specific routing.

* Support issue command automation in workspace composer

- Pass GitHub work items through composer for issue context
- Quick create resolves commands at workspace creation time
- Extract command building and trust logic to dedicated module

* Derive composer hooks by host context for React Doctor

Key loaded hooks and issue commands by execution-host context so
repo/host switches no longer reset derived state in effects (the
static-analysis gate). Also wire SSH-aware workspace targets and
cancel-safe submit settlement for workspace creation.

* Support duplicate repo IDs across hosts in workspace creation

When the same repo exists on multiple hosts (local and SSH), resolve the
workspace creation target to the ready setup on the preferred host instead of
failing closed. Also add resilience to hook checks by clearing the cache on
transient IPC failures.
2026-08-05 02:34:13 -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 2ec36a95c4 test(runtime): pin transport error code/message classification agreement (#12676)
Since #12667, a present error code short-circuits classification: a code that is genuinely transient but missing from `RECOVERABLE_CODES` classifies as FATAL. That is the shape that dead-ends terminal panes — #12650 fixed exactly that for a different error, where a transient failure misclassified as fatal unmounted the Reconnect banner and left recreating the session as the only escape.

Today the code and fragment sets agree. Nothing prevented a future code from being added without a matching entry, and the failure would have been silent.

This pins that agreement: for every reachable transport error, a code that classifies fatal must not carry a message that would have classified recoverable. 57 coded pairs plus 8 code-less ones, derived by invoking the producers where possible so a reworded message updates the corpus instead of leaving a stale copy silently passing. The failure message names the offending fragment and says what to do about it.

Enumeration turned up producers beyond the obvious ones — notably the Tailscale-hinted variants, where `runtime-environment-transport-routing.ts` mutates the message on an already-coded error before it crosses IPC, making those distinct corpus members.

Also documented (not asserted, because it is unreachable today): `runtime_rpc_queue_overloaded` is absent from both host passthrough allowlists, so if it ever crossed `mapRuntimeError` it would flatten to `runtime_error` while keeping its "queue is full" message — precisely the dangerous shape. The queue pool is never instantiated on the server dispatcher, so it cannot happen now.

The known exception is pinned rather than silently exempted: a dedicated test records WHY the guard cannot see `remote_runtime_busy` (fatal by code, matching no fragment, so the two sides have nothing to disagree about). If someone rewords a busy message into connection wording, that test fails and points at STA-3479.

Proven non-vacuous by four separate injections. The only production change is two `const` to `export const`.
2026-08-05 01:01:19 -07:00
Jinwoo Hong 249645832f fix(remote-runtime): stop a slept paired client from erasing its own agent rows (#12664)
After a laptop sleep against a remote machine, the sidebar agent count came back lower than the number of open terminal tabs — rows vanished for panes whose tab and host process were both still alive.

The client mirror deletes a mirrored pane's agent status whenever the host snapshot carries none for it, unless the client's own byte-derived entry is still fresh. But for a remote pane the client is the ONLY writer of that status, and a laptop closed past the 30-minute staleness boundary makes every such entry stale by definition — so the first snapshot after wake erased the sidebar row of every pane the client owned.

Freshness was the wrong gate. It exists to arbitrate between two competing writers, but on the delete branch the host published nothing, so there was nothing to arbitrate and "my status is old" quietly became "delete this pane". The branch now gates on ownership: a pane this renderer claimed and wrote keeps its entry and decays to idle through the normal staleness boundary, exactly like a local pane. Teardown releases the claim, which is how the host takes the pane back.

That reverses a contract #12641 pinned, so its test was updated in place with the reasoning inline rather than deleted — the old premise was that going stale hands the pane back to the host, which does not hold when the host has no value to hand back. The assertion is now stricter: the entry must be retained AND read as stale so consumers render it idle.

This is the sidebar-count half of STA-3107. The blank-terminal half was fixed by #11542 and is proven so: reverting that fix in a six-pane harness makes exactly one of six panes fail to resubscribe while its siblings recover, matching the report.

A remaining gap is documented in the PR: a pane the client never wrote status for stays host-authoritative and can still lose its row. Separating "the host has no opinion" from "the host proved there is no agent" needs the origin marker tracked as STA-3455.
2026-08-05 00:51:44 -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
NeilandOrca aca5e8b5b1 fix(terminal): keep a live TUI's mouse modes across daemon reattach (#12461)
Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:51:33 -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
Jinjing 128a39f7f8 Add host and project filtering to worktree jump palette (#12638)
* Add host and project filtering to the worktree jump palette

Filter the search results by execution host (local, SSH, runtime) and project/repo, with a drill-down options menu, chips for active selections, and overflow hints for large result sets. Filters reset on open to prevent silently hiding results, and stale selections auto-prune. Caps rendered rows per section to prevent DOM bloat from single-character queries. Host badges appear when filtering to clarify which rows survived the cut.

* Add E2E test for worktree jump-palette host filtering

Tests filter interaction via keyboard, filter/project intersection, empty state when filters exclude all results, and ephemeral filter reset on modal close.

* Fix worktree jump-palette filter persistence and search UX

- Persist filter state when filter model changes to prevent dropped IDs from silently re-activating
- Fix result count to distinguish between query matches (all items) vs. empty list (capped sections)
- Improve filter field options: use state for scroller to handle unmount/remount, clamp highlight index to valid range, only reset on query/field change, not re-ranks
- Context-aware space key: allow toggle in listbox only, not in search input (preserves for typing)
- Replace generic "Clear field" translation with field-specific strings to preserve capitalization in non-English languages

* fix(cmd-j): reset filter highlight without prop-change effect

Derive the active option index from field/query identity instead of
resetting it in a useEffect so React Doctor and first-paint stay correct.

* fix static analysis issue

* fix(e2e): rename project entity in jump-palette filter seed

Filter options use project.displayName when a Project exists, so only
renaming the repo left the local option labeled with the path basename.
2026-08-05 00:11:47 -07:00
Neil e5f49e0e1d fix(sidebar): preserve meaningful workspace status indicators (#12658) 2026-08-04 23:58:36 -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 1667b77f0b fix(remote-runtime): keep a remote outage from flooding the error surface and dead-ending the pane (#12650)
When a remote runtime went unreachable (laptop sleep, Tailscale drop), the UI filled with dozens of repeated timeout errors until it was nearly unusable, and the affected terminal then accepted no input after connectivity returned — leaving "close the session and resume it in a new one" as the only escape.

Four causes, three of which were still live:
- Errors accumulated into one ever-growing surface with no de-duplication or cap.
- Queue-overload rejections lose their structured error code crossing the IPC boundary, so they were never classified as recoverable and surfaced raw.
- A transient failure misclassified as fatal called `recovery.cancel()`, setting the pane to an idle phase — which unmounts the Reconnect banner and makes manual retry, online and resume triggers all no-ops. A true dead end, and the reason recreating the session was the only way out.
- Dismissing an error cleared the surface but not the dedup memory, so an identical fatal error recurring in the same outage was suppressed forever while the pane looked healthy; dedup also compared single lines, so multi-line errors never matched and stacked without bound.

The ordinary reconnect loop was already fixed in v1.4.150/160 — bounded backoff, a Reconnect banner and auto-recovery already ship. This fixes what remained.

Note the fix routes fatal resubscribe failures back through the shared terminal error handler: bypassing it had silently dropped stale-handle re-resolution, terminal-gone retirement, SSH-expired recovery and oversized-snapshot suppression — a stuck-pane regression inside the stuck-pane fix, caught in review and covered by 6 dedicated tests.

Verified: reproductions red on main before the fix; after rebasing onto #11542, reverting the dead-end fix still turns its test red. Follow-up STA-3456 tracks preserving typed error codes across the IPC boundary so classification stops matching message text.
2026-08-04 23:21:31 -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
JinjingandOrca bac99c920b Fix combined diff freeze after large diff invalidation (#12615)
* test(diff): repro for STA-3420 combined-diff invalidation freeze

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

* Fix diff-view freeze when large diff invalidated by rebase writes

Staged-diff sections now reload in-place on external file changes instead of remounting every visible Monaco editor and bumping the virtualizer generation, which wedged the renderer during rebase bursts.

* test(diff): calibrate STA-3420 burst assertions against an idle baseline

The burst window's peak lag is dominated by a one-off stall from opening 8x15k-line
Monaco editors, which reproduces identically with invalidation disabled. Measure an
equal-length idle window first and assert p95, sample coverage, and lag relative to
that floor. Adds unit coverage for isUnchangedDiffSectionReload.

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

* fix(diff): keep renderedIndicesRef pure during render

React Doctor blocks ref mutation during render; sync the on-screen
section set in a layout effect instead so static analysis can pass.

* Fix unchanged diff-section reload detection for truncated diffs

When a diff exceeds render limits, content is pruned to '' for memory.
The old check compared content equality, so limited reloads always
appeared changed, triggering unnecessary revalidation that froze the UI.

Compare render-limit metadata instead — it's the sole change signal
and full description of what the fallback banner displays.

Also calibrate STA-3420 e2e assertions relative to idle baseline for
machine independence instead of absolute thresholds.

* fix(diff): defer invalidation reloads for in-flight stale-token loads

When a diff section is invalidated while a large-diff load is in-flight:
- Don't delete the in-flight load from loadingIndicesRef, since a newer load may own it
- Bump the reload token but defer the reload if there's still an in-flight load
- Let the in-flight load settle first, then reschedule the reload at settle-time
- Prevents the freeze by avoiding race conditions that leave sections stuck loading

This fixes STA-3420 where rebase-driven invalidations could hang the diff view.

* test(diff): relax STA-3420 burst assertions to inclusive comparisons

Switch from strict inequality checks (toBeLessThan, toBeGreaterThan) to
inclusive variants (toBeLessThanOrEqual, toBeGreaterThanOrEqual) to allow
measurements landing exactly on the threshold boundaries.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 22:07:30 -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