Commit Graph

8268 Commits

Author SHA1 Message Date
Jinwoo Hong c9485fdded
fix(computer): fence macOS HID coordinate clicks (#12981)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 21:18:52 -07:00
Jinwoo Hong 87d768058c
fix(terminal): recover stale persisted owner bindings (#12976)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 21:18:41 -07:00
Neil 4b2603420e
fix(terminal): protect local ConPTY Ctrl+Enter without breaking TUIs (#12462)
* fix(terminal): gate Ctrl+Enter CSI-u on a negotiated kitty pane

Ctrl+Enter emitted \x1b[13;5u unconditionally, so a pane that never
negotiated the kitty keyboard protocol (local Windows ConPTY, plain
shell) printed the escape verbatim into the prompt. Mirror the
Shift+Enter guard and fall back to the legacy CR every emulator sends
for this chord. Keeps the intercept, so IME commit ordering and the
single-send dedupe still apply.

Fixes #12329

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

* test(e2e): negotiate kitty via PTY output in the Ctrl+Enter spec

The Ctrl+Enter gate reads the PTY-output kitty tracker, which
enableKittyKeyboardReporting never feeds (it writes straight into
xterm's parser), so the spec pressed the chord on a pane the policy
still saw as un-negotiated and got the CR fallback. Negotiate from the
application side like the neighbouring Shift+Enter spec, and reset the
flags afterwards for the serial suite.

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

* fix(terminal): preserve trusted Ctrl+Enter routing

* fix(terminal): scope IME redispatch ownership

* fix(terminal): reject conflicting Ctrl+Enter evidence

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-06 20:07:55 -07:00
Jinjing cb960408f2
fix(mobile): never auto-advertise virtual bridge addresses for pairing (#12962)
* fix(mobile): never auto-advertise virtual bridge addresses for pairing

Container/VM bridges stay manually pickable, but automatic defaults skip them so
QR codes do not race an unreachable direct path. Relay pairs without a local
address; LAN-only and runtime pairing fail closed on bridge-only hosts.

* fix(mobile): never auto-advertise virtual bridge addresses for pairing

- Set endpoint to null when no direct address is advertised, so the QR
  doesn't show an unreachable address to the scanning phone
- Distinguish "No address selected" (bridge exists but not advertised)
  from "No interfaces found" (genuinely nothing to pick)
- Add tests for NetworkInterfacePicker placeholder behavior
2026-08-06 19:17:43 -07:00
Neil f2d62a7887
fix(i18n): localize the status bar Resource Manager tooltip and remote-host count (#12478)
* fix(i18n): localize the status bar Resource Manager tooltip and remote-host count

The Resource Manager tooltip/aria label and the SSH segment's host count were
assembled from bare English literals inside helper functions, so they stayed
English under every non-English UI language while the labels around them
translated. Route them through the catalog with _one/_other plural keys and
whole-line messages (locales reorder and repunctuate the summary), and add
en/es/ja/ko/zh entries.

Root cause of the miss: audit-localization-coverage bailed on any ancestor
binary expression whose operator was not `+`, which hid every string under a
`cond && <JSX/>` guard or a `?? 'fallback'` — including this segment's
'Connecting…'. Only comparison operands are code, so keep `??`, `||` and
`&&` walking, and localize the four real strings that surfaced.

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

* fix(status-bar): flag the space-scan tooltip row instead of matching its English text

The tooltip tinted a row with `line === 'Space scan ready'`, so routing that
copy through the catalog silently dropped the tint in every translated build.
Return `{ text, emphasized }` and let the segment read the flag.

Adopted from #12439 by @smwbev.

Co-authored-by: Evgenii <smwbev@users.noreply.github.com>

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

* fix(status-bar): key Resource Manager tooltip rows by role instead of array index

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-06 19:14:15 -07:00
Jinjing c50a75142d
feat(diff): HTML preview actions in View all combined diffs (#12965)
* feat(diff): HTML preview + always-visible open actions in View all

Expose Open Preview to the Side for HTML sections in combined diffs
when the working-tree file still exists, and keep the open-file
external-link icon visible without hover. Split DiffSectionItem
props/lifecycle helpers to stay under the max-lines limit.

* Fix HTML preview: always-visible buttons and multi-pane group selection

- Make preview buttons always visible (not hover-reveal) for touch support
- Fix event propagation so clicking preview doesn't toggle sections
- Use combined-diff tab's group for sourceGroupId in multi-pane layouts
- Add accessibility label to open-section button
- Support untracked, renamed, and uppercase HTML file extensions

* fix(diff): avoid render-time ref mutation in section model lifecycle

React Doctor fails static analysis when refs are written during render.
Move the disposer ref sync into an effect so the stable callback-ref
still disposes with the latest model paths.
2026-08-06 19:06:04 -07:00
Wooseong Kim e68831f32c
fix(github-project): index fork upstream slugs for project row matching (#12822)
* fix(github-project): index fork upstream slugs for project row matching

Project cards often reference the public upstream repo while the open
clone's origin is a personal fork. Map the parent slug to the same Repo
so selected-repo filters no longer hide every board row.

Preserves origin-based getRepoSlug identity for non-project callers.

Fixes #12647

* fix(github-project): match project rows against fork upstream slugs

Resolve the referenced call to a nonexistent `resolveRepoUpstreamSlug` and
match the persisted `repo.upstream` parent instead of issuing an extra
`github.repoUpstream` RPC per repo on every index build — that lookup shells
out to `gh repo view` for non-forks, so it would have gated the Projects tab
on N network calls. `repo.upstream` is already resolved at repo-add time and
backfilled at startup, so the fix costs no IPC.

Origin matches take precedence over upstream ones so an open clone of the
upstream repo itself is never made ambiguous by someone's fork of it.

Also covers the two surfaces the origin-only match broke alongside the desktop
table: mobile's project row matcher and the store-slice row-mutation routing.

* fix(github-project): scope fork upstream matching by host and selection

Round-1 review fixes on top of the upstream-slug index:

- Apply origin-over-upstream precedence among *selected* repos instead of
  globally. An open-but-unselected clone of the upstream repo was shadowing the
  selected fork, so #12647 still reproduced for anyone holding both — and repo
  selection collapses to one repo per project key, which is exactly that case.
- Scope a fork's upstream identity key to the fork's own origin host.
  Persistence strips upstream.host, so GHES forks never matched their own rows
  and a GHES fork's parent could bind a same-named github.com row.

* fix(github-project): skip the fork alias when its own origin is unresolved

Round-2 review fix. `githubHostFromIdentityKey` cannot tell "origin resolved to
github.com" from "origin did not resolve" — both yield no host. A GHES fork
whose slug resolution had failed (auth lapse, unreachable runtime) therefore
landed in the github.com namespace, so an unrelated public Project row matched
it and Start work opened the wrong clone on the wrong server.

Require a resolved origin before indexing the upstream alias: it is the only
host evidence there is, and a repo with an unresolved origin was already absent
from the origin index, so nothing is lost that origin matching had.

* fix(repos): persist the fork upstream host instead of dropping it

`sanitizeRepoUpstream` kept only `{owner, repo}`, so a fork's parent lost the
server it lives on every time the record round-tripped through disk.

That forced the Project row matcher to re-infer the host from `origin`. The
inference is right for an API-resolved fork parent — `getRepoUpstream` stamps
`origin.host` there precisely because "a fork parent lives on the same server as
the fork". It is wrong for the other branch: a local `upstream` remote carries
its own host, so a github.com clone with a GHES `upstream` remote was indexed
into the github.com namespace, where an unrelated same-owner/name public repo
could claim it and Start work would open the wrong clone.

Keeping the host removes the guess. Absent stays absent, so records written
before this hydrate unchanged and the origin-derived fallback still covers them.
Also fixes the avatar for rehydrated GHES forks, which resolved against
github.com for the same reason.

* docs(github-project): correct upstream host fallback comment

Persistence now keeps non-empty upstream.host; originIdentityKey remains
the host fallback for older records without one (CodeRabbit nit).

* fix(github-project): own slug-index retry timer cleanup

Move the failure-retry setTimeout into its own effect so cleanup always
clears it. Scheduling from the async buildIndex then-handler failed the
react-doctor effect-needs-cleanup gate in static analysis.

* test(github-project): guard the slug-index retry timer, fix the mobile twin comment

Two follow-ups on 52298d82 and 2f89c20d:

- Cover the retry timer both ways: a failed resolution still re-resolves after
  the TTL and recovers the match, and the pending timer is gone after unmount.
  The second fails if the timer moves back into the async then-handler, so the
  property is guarded by more than the lint rule.
- The mobile matcher's comment made the same stale "persistence strips
  upstream.host" claim that 2f89c20d fixed on the renderer side.

* test(github-project): unmount slug-index hooks so React cannot flush after teardown

CI shard `tests node 24 6/16` failed with 10 unhandled
`ReferenceError: window is not defined` traced to this file. The tests mounted
hooks without unmounting, so React scheduler work flushed after the DOM
environment was disposed. All assertions passed; the shard failed on the
unhandled errors alone.

`cleanup()` after each test unmounts the trees. Does not reproduce locally in
isolation — it needs CI's worker pooling and file ordering.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 18:43:28 -07:00
Jinjing 1aaa5cfbd9
Fix cmd+j search ranking to require coverage of meaningful query words (#12954)
* Fix cmd+j search ranking to require coverage of meaningful query words

Extract tokenization logic into a shared module to ensure consistent
ranking across settings and project search bands. Enhance ranking with:
- Coverage requirement: candidates must match most meaningful words, not
  just one (fixes "linear triage" matching all projects on "linear" alone)
- Filler words: ignore navigation words like "open", "go", "the" when
  measuring coverage
- Unicode support: split on Unicode word boundaries, not ASCII only

* Fix cmd+j search to require query coverage and handle Unicode

The search ranking now requires all query words to match candidate values before applying shortcut rules, preventing false positives where middle words could be ignored. Query normalization now iterates over Unicode characters instead of code units to properly lowercase supplementary-plane characters.
2026-08-06 18:40:14 -07:00
github-actions[bot] 7df7aa5fe9 Update README downloads badge 2026-08-07 01:27:54 +00:00
Neil 87d1d1a8dc
fix(terminal): preserve pinned scroll across live redraws (#12783) 2026-08-06 17:57:00 -07:00
github-actions[bot] f23e6f2f90 release: v1.4.176-rc.1 2026-08-07 00:16:17 +00:00
BingZ a025a71447
fix(orchestration): deliver pending mail to already-idle agents (#12584)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 17:08:28 -07:00
Jinjing 8ddf575fe6
Revert "Remove source control group order preference (#12785)" (#12955)
This reverts commit ae1ed5e886.
2026-08-06 16:33:42 -07:00
Brennan Benson 01d3bc5a66
fix(terminal): per-pane WebGL attach latch and fit-anchored reattach (#12945)
* fix(terminal): per-pane WebGL attach latch and fit-anchored reattach

The attach-failure latch was module-global: one pane's failed WebGL context
creation stranded every other pane on the DOM renderer — whose cell metrics
and rasterization differ visibly (bolder, ~5% wider text) — until the next
recovery boundary. The latch is now per-pane.

A successful fit additionally offers an event-anchored reattach: a pane that
is WebGL-eligible but addon-less (late mount that missed the coalesced reveal
repaint, stale fallback) regains WebGL the moment it proves measurable, so a
user resize now heals a DOM-stuck pane instead of leaving it. Failed attaches
still retry only at recovery boundaries. A webgl-fit-attach diagnostic records
each late attach so the stuck state is finally visible in telemetry.

Client-size fit helpers move to pane-fit-client-size.ts to stay under the
pane-fit.ts line cap.

* fix(terminal): refit onto WebGL cell metrics after a fit-anchored attach

The fit that triggers the reattach measures DOM cell metrics; WebGL floors
the device cell width, so healing a DOM-stuck pane left it on the DOM-derived
column count — an unpainted right gutter and a PTY narrower than the pane.
Refit on the next frame, mirroring the dispose-side refreshDimensions.

Also cover the real wiring: the existing fit-anchored tests drive the signal
module directly, so they stay green even if safeFit stops calling it. The new
suite goes through safeFit, which is also what proves the import-time hook
registration works.

* test(terminal): gate the fit-anchored refit frame on a deferred rAF

The existing suites stub requestAnimationFrame synchronously, so the window
in which the refit handle is live never exists there — nothing covered the
two properties that window has to hold. With a deferred stub:

- disposing the pane cancels the refit, so it cannot fit (and forward a PTY
  resize for) an already-disposed terminal;
- the deferred fit re-enters the hook exactly once and settles, so there is
  no fit -> attach -> fit cycle.

Both fail against mutated production code (handle kept out of the
cancellable slot; addon-less guard dropped).
2026-08-06 16:09:38 -07:00
Jinjing fe789c5d52
fix(renderer): contain corrupt lazy chunks when the recovery reload never lands (#12950)
* fix(renderer): contain corrupt lazy chunks when the recovery reload never lands

9 react-error-boundary crash reports across v1.4.171-1.4.175 (macOS, Linux,
Windows) all end the same way: a corrupt lazy chunk fails to import, recovery
requests a reload, the reload never lands, and loadLazyWithRetry re-throws the
raw SyntaxError/TypeError. RecoverableRenderErrorBoundary only suppresses
LazyChunkLoadError, so the raw error files a user-facing crash report.

LazyChunkLoadError was unreachable in production. Its precondition is a guard
written by a *different* document ('reload-landed'), but the finally block
clears that guard before the throw, so the only path that could construct it
never ran. Confirmed by the shipped bundles: 16/16 lazy_chunk_reload_vetoed
breadcrumbs carry outcome=never-landed, zero carry any other outcome, and no
bundle contains a boundary-degraded breadcrumb.

Route every exhausted-recovery path through exhaustedRecoveryFailure() so an
attempted-and-failed recovery yields a LazyChunkLoadError the boundary can
contain, and record a lazy_chunk_recovery_exhausted breadcrumb carrying the
call site, the real chunk error, and the outcome.

Deliberately unchanged: when recovery is never *attempted* (no window/SSR,
blocked sessionStorage, guard write failure) the raw error is still thrown so
normal crash reporting is unaffected. Only isKnownDynamicImportFailure matches
are contained, so module logic bugs keep reporting.

* perf(renderer): trim redundant work on the lazy-chunk failure path

Hoist the dynamic-import message patterns to module scope so classification
stops allocating seven RegExp objects per call, thread the already-computed
classification into exhaustedRecoveryFailure so the guard-not-landed path does
not re-run it, and bound recordedExhaustionKeys the way the breadcrumb and
renderer-error key stores are bounded, since error.name is library-controlled.

Failure path only; the success path is unchanged.

* refactor(renderer): remove a transposition trap on the lazy-chunk failure path

exhaustedRecoveryFailure ended in two adjacent booleans with opposite
consequences: transposing them would have returned the raw SyntaxError and
silently restored the crash this branch fixes, with no test able to catch it
(the only call site passed true for both). The isChunkFailure parameter saved
one regex scan on a path that only runs after a 10s reload wait, so drop it.

Also evict recordedExhaustionKeys oldest-first instead of clearing wholesale,
matching the breadcrumb and renderer-error key stores the comment cites, so an
overflow cannot re-open the entire set to a repeat burst.

* test(renderer): cover the exhaustion dedupe bound

The bound had no coverage, unlike the crash-breadcrumb store it mirrors, so a
refactor could drop it or invert the comparison with every test still green.
Drive 200 distinct error names through the contained path and assert the set
stays capped. Also move MAX_RECORDED_EXHAUSTION_KEYS above the comment that
describes the set, not between them.

* test(renderer): pin the exhaustion eviction policy, not just the cap

The bound test asserted only the size cap, so it stayed green under the old
wholesale clear(): after 200 distinct keys a clear-on-overflow leaves 72, which
still satisfies the cap. Replay a key that oldest-first eviction retains and
assert it emits no second breadcrumb — that fails under clear(), which would
otherwise silently re-open the whole set to a repeat burst and flush the
30-entry ring the dedupe exists to protect.

* refactor(renderer): cut the breadcrumb machinery down to the actual fix

The lazy_chunk_recovery_exhausted breadcrumb was an optional addition that paid
for itself in complexity and nothing else: it needed a dedupe set to avoid
flushing the 30-entry ring, the set needed a bound because error.name is
library-controlled, the bound needed an oldest-first eviction policy, and that
needed two more tests plus a boolean parameter that review flagged as a
transposition trap. On the dominant never-landed path it did not even fire,
because lazy_chunk_reload_vetoed already records the same reloadKey, message and
outcome.

Drop it. Observability on every path returns to the main baseline, and the fix
is what it always was: name an exhausted recovery so the boundary can contain
it. Also revert the unrelated regex hoist -- its only caller is the failure
path, so the saved allocations are noise.

* Verify ordinary errors bypass lazy chunk containment

Add test ensuring module evaluation bugs still surface despite
never-landed reload attempts. Clarify containment scope: recovery
only applies to known dynamic-import failures, not ordinary errors.
2026-08-06 15:36:21 -07:00
Brennan Benson b9e9811924
fix(permissions): use standard macOS Local Network request flow (STA-3505) (#12833)
* fix(permissions): surface macOS silent Local Network denial with diagnostic and workaround (STA-3505)

On macOS 27 beta, NECP silently denies Orca's whole process tree Local
Network access: no prompt fires, the app never appears in System
Settings, and terminal child processes fail with EHOSTUNREACH. The
Settings trigger swallowed the probe's socket error and reported
'unknown' + a 'Permission request sent' toast, indistinguishable from
success.

Classify the mDNS probe outcome (EHOSTUNREACH/EHOSTDOWN -> denied,
clean send -> granted, else unknown), remember the verdict for the
status chip, and render an inline diagnostic with the documented
NECP re-evaluation workaround when denial is detected.

* fix(permissions): avoid false Local Network grants

* fix(permissions): use standard Local Network request flow

* feat(permissions): add local network connection test

* fix(permissions): nest local network connection test

* fix(permissions): collapse connection test by default

* fix(permissions): emphasize connection test action

* fix(permissions): restore outlined connection action
2026-08-06 14:10:26 -07:00
github-actions[bot] ddf64199fa release: v1.4.176-rc.0 2026-08-06 20:57:02 +00:00
Brennan Benson b81450f085
fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494) (#12832)
* 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.

* fix(azure-devops): constrain preview retry and base override
2026-08-06 13:42:37 -07:00
Brennan Benson 18b27ac11c
fix(terminal): show AI Vault conversation names in tabs (#12778)
* fix(terminal): use provider-native session titles

* refactor(terminal): source session names from AI Vault

* fix(tabs): harden AI Vault title sync
2026-08-06 13:29:06 -07:00
Brennan Benson 99d2f03ca7
fix: scope idle agent setting to dashboard (#12941) 2026-08-06 13:28:16 -07:00
BingZ 74e1678d4b
test(runtime): pin the fail-closed contract for bare Cursor titles (#12816)
Reverts the classification change and keeps behavior at base.

cursor-agent's native OSC title is the bare literal "Cursor Agent" and never
carries a status word, so it names the agent without proving one is present.
The title tracker drops it live, so main records it only when the stale-working
timer strips the spinner off the synthesized "⠋ Cursor Agent" — and that fires
both when Cursor parks idle and when cursor-agent exited and the shell reclaimed
the pane. The two states are observationally identical: same title, same null
foreground read.

Classifying it as an agent therefore removes a refusal rather than adding
evidence. Guarded sends auto-submit Enter, so the false positive types into the
user's shell. A null foreground is also not "unreadable" on the default local
provider, which returns null when the pty is gone.

hasPty, probePtyLiveness, hasChildProcesses and inspectProcess were each checked
as corroborating signals; none separates alive-with-agent from alive-with-shell
when the foreground read is unavailable.

Tests pin every no-evidence branch fail-closed and document the mechanism, so
both attempted fixes fail loudly if reintroduced.

Real gap tracked in #12946.
2026-08-06 13:15:48 -07:00
Brennan Benson f6d0bde6fb
fix(mobile): choose host for new workspace (#11647)
* fix(mobile): choose host for new workspace

* fix(mobile): close stale workspace host picker

* fix(mobile): disambiguate workspace host choices

* fix(mobile): keep host endpoint paths private

* fix(mobile): redact invalid host endpoints

* fix(mobile): handle opaque host endpoints

* fix(mobile): announce host picker options

* fix(mobile): harden workspace host picker

* fix(mobile): preserve host through workspace creation
2026-08-06 11:15:36 -07:00
Brennan Benson 92f759bb51
perf: bound repeated watcher, Session History, and terminal work (#12828)
* perf: bound repeated watcher, vault, and terminal work

* fix(terminal): preserve redraw recovery while bounding fit retries

* fix(watcher): retain structural fallback after crash fuse

* fix(ai-vault): preserve forced scan budget
2026-08-06 10:37:52 -07:00
Neil f71373953b
fix: prioritize filenames in cmd+p quick open (#12679)
* fix: show full paths in quick open results

* refactor: use native file path tooltips

* fix: position file path tooltips

* refactor: share the cursor path tooltip with quick open

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

* fix: let path tooltips run wider before wrapping

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-06 03:26:20 -07:00
Neil ead6b8e225
fix: prioritize filenames in new-tab file results (#12670)
* fix: prioritize filenames in new-tab file results

* fix: preserve root separator in filename-first paths

* refactor: use native file path tooltips

* fix: position file path tooltips

* fix: use the native OS tooltip for new-tab file paths

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

* fix: show new-tab file paths in a system-style tooltip

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

* fix: anchor new-tab path tooltip to the cursor

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

* fix: tighten cursor tooltip to file rows and design tokens

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-06 02:35:06 -07:00
erish ac961d6329
fix(repo-icon): fall back to a lucide icon when an image icon fails to load (#12867)
* fix(repo-icon): fall back to a lucide icon when an image icon fails to load

Private-mode GitHub Enterprise avatars need a logged-in web session, so the
stored avatar URL fails to load and the image branch rendered blank space.
The lucide and missing-icon paths already fall back to Folder; the image
branch had no equivalent.

Track the failed src in state so a repo switched to a different icon still
renders that icon instead of staying on the fallback.

Fixes #11211

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(repo-icon): assert the specific fallback icon instead of any svg

The fallback and unchanged-icon tests only checked that an svg rendered, so
they passed even if the wrong icon came back. Assert the lucide class name,
and cover an unknown lucide name falling back to Folder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 02:26:33 -07:00
dcieslak19973 b5ad2ac163
fix(ai-vault): group OMP task subagent transcripts under their parent session (#12803)
* fix(ai-vault): group OMP task subagent transcripts under their parent session

OMP persists task-child transcripts inside the parent session's same-named
artifact directory (<stamp>_<uuid>/), and discovery scanned them as ordinary
top-level sessions - a coordinator's history drowned under its own workers.

Extend the existing Claude subagent model to OMP, classifying purely by the
artifact-dir layout (never by a transcript's parentSession field, which also
describes non-task lineage):

- prune artifact dirs from the top-level scan (name-pattern predicate)
- count direct-child transcripts onto the parent row (local readdir; remote
  walks partition their listing instead, mirroring Claude's SSH posture)
- list children on demand via the existing listSubagentSessions IPC, titled
  by their coordinator-given task label and linked to the layout-derived
  parent id
- refresh the count on zero-turn cache reuse, matching Claude
- extract session-scanner-roots.ts so the renderer-supplied-path allowlist
  for both agents lives in one module

Fixes #9330

* review: harden OMP subagent classification and cover its uncovered branches

Prune predicate now skips depth 0 (the workspace dir), so a workspace whose
name happens to look like a session stem keeps its sessions. Drop degenerate
OMP roots in ompSessionsRootDirs: OMP_CODING_AGENT_DIR='/' normalizes to '',
which resolve()s to the process cwd and would have allowlisted it for the
renderer-supplied subagent path.

Rename session-scanner-omp-subagents.ts to -omp-subagent-transcripts.ts so it
mirrors Claude's transcripts/lister split by role rather than inverting it.

Correct two comments that asserted things the codebase contradicts: OMP task
children do carry their own sessionId and would resume by path (OMP's own
picker globs `*/*.jsonl`, so it never offers them either), and workspace dir
names are not uniformly dash-prefixed.

Cover branches the change added with no test: the remote/SSH partition wiring,
the IPC `omp` gate and per-agent allowlist, the parse-cache zero-turn recount,
and the executionHostId disk-ownership guard. Extract the remote scanner's
in-memory provider into a fixtures module to stay under the max-lines cap.

* review: note why child rows carry an unrendered grandchild count

* review: describe the real OMP grandchild layout in the pattern comment

---------

Co-authored-by: Dan Cieslak <dcieslak19973@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 01:29:19 -07:00
Jinjing c3db2b89fc
Demote address picker to optional disclosure on Orca Relay (#12796)
* refactor(mobile): demote address picker to optional disclosure on Relay

Relay provides remote access without requiring a specific local address,
so hide the picker behind a disclosure to keep the direct fast path
accessible without visual clutter. Reposition Sign in between the Relay
and LAN options to clarify it's Relay-specific. Keep custom addresses
always visible and force the disclosure open when settings search
targets the address picker.

* refactor(mobile): improve relay pairing guide and interface ranking

- Rank Docker/VirtualBox bridges below real LAN addresses so they're never auto-advertised as the default
- Clarify UI copy: 'Local network address (optional)' → 'Direct connection on this network'
- Better explain direct connection vs Relay roles and when each is used
- Fix Relay unavailability to be a build property, not dependent on current selection

* refactor(mobile): reframe local network address as optional in relay pai

Demote the address picker from primary action styling to an optional
disclosure with quieter visual treatment. Update messaging from "Direct
connection on this network" to "Also use a faster local path" to
clarify Relay is the default path and local addressing only applies
when nearby. Add explanatory hint text to set expectations that Relay
remains available when away.
2026-08-06 00:34:42 -07:00
Jinjing 1251e5530f
Show SSH worktrees immediately via persisted metadata (#12799)
* Retire SSH worktree metadata an authoritative scan proved gone

The metadata fallback's protection against resurrecting externally deleted
worktrees lived only in renderer module state, so it died on every reload
while the SSH WorktreeMeta it guarded against persists forever
(gcStaleWorktreeMeta exempts any repo with a connectionId, because a local
existsSync cannot probe a remote path). Repro: `git worktree remove` on the
SSH host, let the authoritative scan purge the row, restart — the startup
fetch runs before SSH connects and the fallback re-lists the deleted
worktree as a ghost row.

Chose option (a), deleting the stale persisted meta in main, over persisting
the removal memory: the metadata is the thing that outlives the worktree, and
Orca's own removals already delete it (removeWorktreeMetadataAndTransientState),
so external removals now converge on the same end state instead of accumulating
a second, parallel tombstone list that would itself need eviction. The
in-session memory stays for the window before the async delete lands.

New `worktrees:forgetRemovedForExecutionHost` only accepts SSH hosts, requires
an exact repo owner, skips metas owned by another host, and refuses folder
repos — a folder workspace's meta IS the workspace record (gcStaleWorktreeMeta
skips those keys for the same reason) and no remote scan can retire one. The
renderer only calls it from the authoritative-removal path, so a mere
disconnect never deletes anything.

Also:
- hoist resetAuthoritativelyRemovedWorktreeMemoryForTests into a top-level
  beforeEach; removeWorktree writes that memory too, so suppression could leak
  across describes and silently hide a row.
- cover the requireAuthoritative gate that skips the fallback, which had no test.
- replace the raw NUL byte committed inside the coalesce-key template literal
  with a \0 escape; it made the file scan as binary to grep/ripgrep.

* test(worktrees): verify non-authoritative fallback skips removal

The non-authoritative fallback must not trigger worktree cleanup when it observes an absence — only an authoritative scan should. Tighten the expectation to ensure cleanup happens exactly once, when new data arrives after the connection state changes.
2026-08-06 00:33:12 -07:00
Neil a7ffb244e4
perf(terminal): bound the reattach payload agent-signal scan (#12681)
hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the
entire reattach payload so it could read the last header plus 5000 chars. On a
2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work —
~75% of what xterm then spends parsing the same bytes — and the miss case paid
it in full for a result that is always false.

Two changes, both matching existing in-tree precedent: bound the scan to a
256KB tail (as the kitty tracker already bounds its own scan), and strip via
the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop,
which is also faster in V8 because it copies spans rather than building a rope
per character.

  2MB snapshot, header hit   17.5ms -> 0.80ms  (22x)
  2MB snapshot, miss          8.7ms -> 0.52ms  (17x)
  200KB snapshot, header hit  1.5ms -> 0.62ms  (2.4x)

config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and
asserts every candidate agrees with the baseline before timing it. It also
records a negative result: porting the daemon mouse mirror's includes()
pre-filter to the kitty tracker makes reattach slower, because snapshots always
contain the introducer.

Adds guards for the two behaviours a future shortcut would silently break: a
CSI-split header must still match, and a header behind the tail bound must not.
Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned.

Co-authored-by: Orca <help@stably.ai>
2026-08-06 00:23:57 -07:00
Jinjing 39ba201ed2
fix(source-control): prevent long branch names from overlapping chip (#12842)
- `truncate` has no effect on inline boxes, so long branch names would
  overflow their flex item and run under the line-total chip
- Adding `block` display forces text truncation with ellipsis instead
- Increase gap from 1.5 to 2 so ellipsis doesn't visually merge with chip
2026-08-06 00:11:01 -07:00
Brennan Benson a2d438db17
fix(daemon): detect severed macOS TCC attribution behind terminal automation denials (STA-3491) (#12848)
* fix(daemon): detect severed macOS TCC attribution and surface daemon-restart remedy (STA-3491)

macOS pins the detached PTY daemon's TCC responsible process to the app
binary that forked it. Once that binary is deleted (packaged updates
replace the bundle), Accessibility/Automation grants on Orca silently
stop covering every daemon-hosted terminal: osascript/System Events
fails with -25211 no matter what the user grants.

- record spawnerExecPath in the daemon pid file at fork
- adoption checks it: severed + 0 live sessions -> replace the daemon
  (reason severed_tcc_attribution); live sessions are preserved
- Settings (Developer Permissions + Manage Sessions) show a visible
  banner pointing at Manage Sessions -> Restart while severed

* fix(daemon): harden TCC attribution recovery
2026-08-05 23:54:30 -07:00
Neil f82d5c3e6c
fix(terminal): clear stranded link hover tooltip (#12786)
* fix(terminal): clear stranded link hover tooltip

* fix(terminal): declare the tooltip reserve var where it resolves

--orca-terminal-link-tooltip-height was declared on .pane-manager-root, a
class no live element carries, so both .xterm-container height calc()s were
invalid at computed-value time and collapsed to height:auto — the element
FitAddon measures, making rows a fixed point.

Also isolate _clearCurrentLink() so a throwing provider leave() cannot skip
the cache invalidation, and bound the e2e gap assertion on both sides.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-05 23:48:44 -07:00
Brennan Benson 6182ca4b04
fix(mobile): expose host card actions (#11648)
* fix(mobile): expose host card actions

* fix(mobile): preserve host status accessibility

* fix(mobile): make host path labels speakable

* fix(mobile): preserve host card accessibility details

* fix(mobile): remove redundant host card chevron
2026-08-05 23:39:20 -07:00
pstanton237 ca187c7307
fix(menu): restore paste in macOS native dialogs (#12645)
* fix(menu): restore paste in macOS native dialogs

* test(menu): pin platform for paste routing coverage

CI runs unit shards on ubuntu-latest only, so the unpinned paste-routing
test asserted "no native paste" vacuously: a double-route regression
passed green on Linux. Cover darwin/linux/win32 explicitly so the
exactly-once contract holds on every platform, and assert the item exists
in the negative-only cases so a rename cannot pass them silently.

Also document why the native first-responder fallback exists.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 23:17:34 -07:00
Brennan Benson a30e3b9f61
feat(dashboard): add experimental agent map view (#12168)
* feat(dashboard): add experimental agent map view

* fix(dashboard): harden agent map behavior

* fix(dashboard): harden agent map recovery

* fix(dashboard): close map selection on view change

* fix(agent-map): center sparse layouts

* fix(agent-map): align completion and workspace actions

* Polish agent map interactions and repo labels

* feat(agent-map): add worktree lineage and project actions

* fix(agent-map): use marker for unread agents

* fix(agent-map): compact orchestrated families

* fix(dashboard): harden agent map actions and layout

* fix(agent-map): bound layout work and preserve interactions

* fix(agent-map): move unread marker to ring top-right

* fix(agent-map): seat unread marker on the ring's top-left edge

* 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.

* refactor(agent-map): fold the map's filter rail into the shared toolbar filter

The rail duplicated the toolbar's project filter and cost the canvas 14rem of
width on the surface that needs it most. Agent states move into the toolbar's
Filter dropdown (map view only — the board's columns already separate them) and
count toward its badge; project filtering falls back to the toolbar's own. Show
all is the dropdown's Clear all, and Fit already lives in the viewport controls.

* fix(agent-map): isolate map work from main renderer

* perf(agent-map): stream status updates to popout

* fix(i18n): add agent map catalog entries

* feat(agent-map): glow working entities

* fix(agent-map): prioritize attention ring status

* fix(agent-map): distinguish subagent connectors
2026-08-05 22:57:16 -07:00
Wooseong Kim 12472b8a63
fix(skills): match official skill files despite local sidecars (#12812)
* fix(skills): match official skill files despite local sidecars

Scope known-snapshot matching to manifest-listed files so agent-written
sidecars (e.g. agents/openai.yaml) no longer mark a package unrecognized
and block updates when official bytes still match.

Preserves fail-closed detection when a listed file's content drifts.

Fixes #12694

* fix(skills): scope lock trust and convergence to official files too

Sidecar tolerance stopped at the snapshot match, leaving three disk-vs-official
comparisons still judging the whole folder.

The lock-comparable hash covered every observed file, so a clean update beside
agents/openai.yaml reported as failed and read 'may be modified'. It is now
carried both whole and scoped to the current bundle's paths, and either may
satisfy the lock: the sidecar case only ever matches scoped, while an upstream
revision that ADDS a file only ever matches whole, so publishing one alone
would trade this bug for #11220.

Convergence re-derived the disk revision from that same whole-folder digest,
which no revision matches once a sidecar lands, retiring the stuck-lock gate
and arming an update the command provably cannot perform; it now honours the
revision observation already resolved.

Subset matching also let an older revision launder drift on a file the current
bundle lists, since that revision does not list it and so read it as a
neighbour. Identity now keys tolerance on what the current bundle owns.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 22:43:22 -07:00
Brennan Benson ab1bf1aff5
fix(settings): recover HTTP proxy settings from safeStorage failures (STA-3442) (#12838)
httpProxyUrl is the only network setting stored via safeStorage. Two
failure modes silently killed the configured proxy on macOS:

- A keychain reset/denial makes decryptString throw at load; the raw
  ciphertext then masqueraded as the configured proxy URL, so
  applyElectronProxySettings silently fell back to DIRECT and the
  garbage re-persisted forever (no self-heal).
- safeStorage.isEncryptionAvailable() throwing (keychain/API errors,
  pre-ready use) was uncaught in encrypt/decrypt, failing the entire
  state save - the data file never gained the proxy keys at all.

Load now validates the decrypted value and clears undecryptable
ciphertext (plaintext URLs still pass, preserving the pre-encryption
upgrade path), the availability check is exception-safe, and startup
logs when persisted proxy settings are invalid instead of silently
using direct networking.
2026-08-05 22:40:27 -07:00
Brennan Benson 790ade578a
fix(browser): recover browser tab when guest WebContents is destroyed (STA-3448) (#12831)
* fix(browser): recover browser tab when guest WebContents is destroyed (STA-3448)

A <webview> whose guest WebContents died without render-process-gone
(detach/reattach race, guest-side close) stayed attached and painted
black forever; reload and focus on the dead guest threw uncaught.

- listen for the webview 'destroyed' event at both layers, mirroring
  render-process-gone: registry marks recovery pending (survives pane
  unmount), BrowserPane triggers guest recovery immediately
- route reload-on-dead-guest into guest recovery instead of throwing
  (toolbar, Cmd+R renderer+IPC paths, context menu)
- guard webview.focus() against Electron's null-internals throw

* fix(browser): close guest reload destruction race
2026-08-05 22:27:38 -07:00
Brennan Benson 215604f458
fix(tasks): hold dialog-confirmed issue state over stale Tasks list refetches (STA-3343) (#12830)
* fix(tasks): hold dialog-confirmed issue state over stale list refetches (STA-3343)

Closing an issue from GitHubItemDialog patched workItemsCache directly
with no mutation-registry record, so a search-lagged Tasks refetch
(GitHub search index eventual consistency + gh's ~120s URL cache)
silently reverted the row to Open. Record the confirmed state as
registry authority (same mechanism the list-row mutations use) so list
fetch paths re-assert it until search catches up; quiet adopt still
releases it on match, so external reverts win once the index is fresh.

Covers issue close/reopen, PR close/reopen, and PR merge in the dialog.

* fix(tasks): preserve newer state authority on rollback
2026-08-05 22:25:01 -07:00
Brennan Benson c2da0e47f9
fix(computer): deliver macOS coordinate clicks via the HID event tap (STA-3433) (#12839)
Mouse events posted with CGEventPostToPid reach the target app with no
window association, so AppKit never routes the press to a view: hover
states fire but the control is never activated, and the mouseUp is
dropped outright when posted back-to-back. Post click events to the HID
event tap instead (as keyboard synthesis already does), pace them, and
stamp mouseEventClickState so multi-clicks register.

Synthetic clicks now also report verification unverified/synthetic_input
from the helper itself, matching the other synthetic actions.
2026-08-05 22:14:18 -07:00
Brennan Benson 89f00658d2
fix(mobile): bound home host auto-connect fanout (#11642)
* fix(mobile): bound home host auto-connect fanout

* fix(mobile): clarify bounded host connection state

* fix(mobile): close unused settings host clients

* fix(mobile): close released host clients

* fix(mobile): sync host release policy in effect

* fix(mobile): preserve focused host client ownership

* test(mobile): cover manual Home client release

* Fix React Doctor array type check
2026-08-05 22:08:40 -07:00
Brennan Benson cd8c66551a
fix(agent-hooks): resumed Claude Code session gets its sidebar agent row at SessionStart (STA-3386) (#12859)
* fix(agent-hooks): give resumed Claude sessions a sidebar row at SessionStart (STA-3386)

Claude's hook set never registered SessionStart and normalizeClaudeEvent
dropped it at ingest, so a resumed session that idled produced zero hook
traffic and earned no sidebar agent row until the first prompt.

- Register SessionStart in CLAUDE_EVENTS (local + remote installs).
- Map lead SessionStart (startup/resume/clear) to an idle 'done' row,
  resetting stale roster/task/cron/tool/prompt state like the Codex path;
  compact restarts and child-attributed SessionStart stay dropped.
- Thread hookEventName through the agent-status IPC payload so the
  completion coordinator can tell a session connect from a turn result;
  a SessionStart 'done' no longer raises agent-task-complete.

* fix(agent-hooks): mark SessionStart rows as session boundaries, not completions (STA-3386)

Review follow-up: represent the idle connect as a first-class
sessionBoundary flag on the status payload instead of gating one
renderer consumer on hookEventName.

- sessionBoundary rides AgentStatusPayload/AgentStatusEntry (done-only,
  clamped like interrupted); drops the hookEventName IPC threading.
- Completion-reactive consumers ignore session boundaries: the
  completion coordinator (task-complete notifications), automation
  dispatch observers (a connecting agent no longer completes the run
  and closes its tab), activity unread counts, and the dashboard
  finished timestamp; the status slice keeps boundaries out of
  stateHistory and preserves the flag across done->done repaints.
- SessionStart sources are allowlisted (startup/resume/clear) so
  compact restarts or unknown sources fail closed mid-turn.
- A live SessionStart now un-retires a reusable pane like a fresh
  prompt, so resume-in-reused-pane earns its row too.

* fix(agent-hooks): keep session-boundary dones out of teardown and completion history (STA-3386)

Review round 2:
- A boundary done no longer deletes the pane's launch-config registry
  entry, so a resumed idle TUI keeps its registered-launch-agent
  identity evidence.
- A boundary landing on a REAL done pushes that completion into
  stateHistory so the finished timestamp and unread badge survive a
  resume//clear right after a finish.
- The done->done flag carry yields to turn evidence (assistant message
  or changed prompt) so a genuine completion can never be suppressed.
- Star-nag value-moment observer and the server's OSC-equivalence
  dedupe now discriminate the flag.

* fix(agent-hooks): keep a displaced completion unread in the sidebar badge (STA-3386)

Review round 3: sidebar-badge mode counts only the live entry, so a
session boundary landing on an unacknowledged completion silently
dropped the sidebar badge while the agent-events count kept it. Count
the displaced completion from history for boundary rows, and pin the
behavior with countActivityUnread tests.

* fix(agent-hooks): prevent SessionStart completion side effects (STA-3386)

* fix(agent-hooks): preserve SessionStart through renderer IPC (STA-3386)
2026-08-05 22:06:36 -07:00
Brennan Benson 3d6a98a834
fix(native-chat): report the current Claude model from the terminal frame (#12860)
* fix(native-chat): locate Claude's model row by frame structure

The scraper assumed the model descriptor sits within three rows of the
`Claude Code vX` line. It does not: Claude prints it near the bottom of the
startup frame with the welcome art and release-notes panel in between — eight
rows down at 100 columns. Narrow panes degrade the frame further, dropping the
version from the title row entirely below ~70 columns and wrapping the billing
tail onto its own row. Any one of those made the scrape return null, so the
model picker showed no current selection at all.

Search the frame from its bottom border upward for the row carrying model
metadata, read only the leftmost frame cell so release-notes prose can never
win, accept the frame corner as header proof when the version is gone, and
tolerate the effort suffix being elided to an ellipsis. Catalog families now
match as a leading word, which both survives the resolved-name suffix
("Opus 5 (1M context)") and keeps custom slugs like company/my-haiku-v2 from
being claimed as haiku; an unrecognized name is reported as a custom model.

Fixtures are real: captured from a live claude 2.1.220 by replaying the PTY
bytes through @xterm/headless and serializing exactly as TerminalPane does.

* fix(native-chat): resolve the scraped model against the host's real catalog

The scraper matched the static seed while the picker lists what #12369
discovers from the host CLI, so the two spoke different id spaces. On a current
CLI `list_models` returns `opus[1m]`, `sonnet`, `sonnet[1m]`, `fable` and
`haiku` — no plain `opus` — while the seed only knows families. Reporting
`opus` therefore selected a row the picker had to invent, dropping the host's
own effort and fast-mode descriptors with it. Locating the model row correctly
made this the normal case rather than a rarity, since the scrape now succeeds.

Resolve against the discovered list first, falling back to the seed for aliases
a host no longer lists and to the raw name for genuinely custom models. Matching
requires the family to lead as a whole word and the label's remaining tokens to
appear in order, so `Opus 5 (1M context)` picks `opus[1m]`, plain `Sonnet 5`
keeps `sonnet` instead of being captured by the 1M-context row, and
`opus-internal-v3` stays custom. Most specific label wins.

The hook keeps the screen that parsed so a discovery landing after the first
read re-resolves it, rather than stranding a family id once the frame has
scrolled out of the buffer.

* fix(native-chat): identify option-less models on narrow panes

Live capture at 60 columns: a Haiku session prints a bare `Haiku 4.5` row with
its billing wrapped to the next line. No middot, no effort suffix — nothing
marks it as the model, so it reported nothing. Claude always closes the frame
with the working directory and prints at most the descriptor plus a wrapped
billing line above it, so fall back to walking up from there when no row
carries descriptor metadata. The height bound is what keeps the walk from
climbing into the welcome art.

* test(native-chat): pin re-resolution when discovery lands after the read

Covers the wiring the parser tests cannot reach: the frame is visible at mount
and gone by the time the host's model list arrives, so only the cached screen
can drive the second resolve. Fails against a listener that merely replaces the
models.

* fix(native-chat): prevent stale Claude model reports
2026-08-05 21:54:45 -07:00
Jinwoo Hong b0ba51831c
Add per-worker model and effort overrides (#12851) 2026-08-05 21:17:45 -07:00
Neil 7da9368b78
fix(terminal): fence detached daemon endpoint ownership (#12709)
* fix(terminal): fence daemon endpoint ownership

* fix(terminal): clean failed daemon PID claims

* fix(terminal): close daemon ownership review gaps

* test(daemon): release startup IPC in boot smoke

* test(daemon): mirror production stdio in boot smoke

* fix(daemon): exit after rpc shutdown cleanup

* fix(terminal): make the socket name the daemon endpoint authority

The reported failure was a live daemon hosting PTYs that nothing could
reach: terminals acknowledged input and never ran it, listings diverged
from reality, and restarting the app never helped because the detached
helper survived. The ownership fence added for it could not fire in the
sequence that produces the split brain.

libuv unlinks the pathname a server bound to when that server closes,
with no ownership check. A daemon that lost its endpoint name therefore
deleted whichever socket then sat at that path — including a live
replacement's — stranding a daemon that still hosted every session.
Bind a private same-directory name and hard-link it into place instead:
libuv can only ever unlink our own bind name, the exclusive link is a
kernel-enforced endpoint claim, and the canonical name is removed only
under an inode ownership check. The bind name replaces the basename
rather than extending it, so it cannot overflow sun_path.

killStaleDaemon removed the PID record unconditionally immediately
before every fork, so the exclusive PID claim was always uncontested at
bind time. It also unlinked a live daemon's endpoint whenever a connect
probe merely timed out, and treated a `ps` timeout as proof of PID
recycling. Now only positive evidence of a dead endpoint authorizes
reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon
that cannot be proven stopped keeps its record and endpoint while the
launcher refuses to fork beside it.

A daemon whose endpoint was taken over now retires itself, draining
rather than killing, so an unreachable orphan stops being permanent.

A repaired PID record re-derives entryPath, appVersion and the Linux
incarnation markers from the authenticated owner instead of dropping
them; without appVersion a healthy daemon read as a permanently stale
bundle and, on Windows, went unpinned against daemon-host pruning.
Repair failure now fails open — abandoning a healthy daemon over a pid
file write cost every persistent terminal on the machine.

Also: treat only ENOENT as an unclaimed record so a Windows file lock is
not reported as an ownership conflict; settle start() before close() so
an accepted connection cannot defer it forever; sweep abandoned claim
and bind names; and type the endpoint-identity seam so a rename cannot
silently disable the fence.

Adds a real-process handover smoke that reproduces the failure with two
daemons racing one endpoint, and wires it into the native-smoke job.

* fix(daemon): retire only on proven endpoint ownership loss

The ownership watchdog read a null identity for any stat failure, so a
transient EACCES or EIO on the runtime directory would retire a daemon
that was still serving every terminal on the machine. Distinguish "the
entry is gone" from "the probe failed" and act only on the former.

Also require the loss to persist across two polls: a replacement
publishes by unlink-then-link, and a single observation can land in that
gap.

* fix(daemon): source repaired ownership metadata from the authenticated hello

Adversarial review found three defects in the previous two commits.

Re-deriving entryPath from the owner's command line truncated it at the
first space. A command line is a single space-joined string, so
`C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back
as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats
a present entryPath as authoritative, so a healthy daemon read as
`different_app_path` and was killed and re-forked — worse than the
missing-metadata case the derivation was added to fix. Carry entryPath
and appVersion as optional fields on the daemon hello identity instead:
the daemon already has both from its own argv, and per
docs/reference/remote-wire-compatibility.md a new optional field is safe
because every reader falls back when it is absent. This also removes a
synchronous `ps` spawn from the Electron main thread during startup.

`start()` rolled back the PID record even when it never published one.
Losing the endpoint link now runs that path, and the ownership-checked
unlink briefly renames the incumbent's record aside — enough to strand a
live daemon's ownership. Roll back only what we actually wrote.

publishDaemonSocketPath read its identity from the canonical name after
linking, so a concurrent unlink returned null: no ownership watchdog and
no endpoint cleanup on any shutdown path. Read it from the bound name
before linking, which shares the inode.

Refusing to fork beside an unconfirmed daemon left the user with no
daemon at all and no in-app recovery, since restart re-entered the same
fence. We have just proved something answers the endpoint, so adopt it
in degraded mode: live sessions keep working, fresh terminals run
locally. SIGTERM is also individually guarded now — an EPERM fell into
the blanket catch and reported "nothing alive", authorizing the very
duplicate this fence exists to prevent.

Also reset the ownership-loss streak on an inconclusive probe so the
confirmations are consecutive, and sweep scratch names before the launch
so a failed launch still reclaims them.
2026-08-05 21:12:32 -07:00
Brennan Benson b521837481
fix(mobile): keep hosts visible when credentials are unavailable (#12775)
* fix(mobile): keep hosts visible when credentials are unavailable

* fix(mobile): guard unavailable host recovery

* fix(mobile): protect replacement credential writes

* fix(mobile): retain superseded cleanup intents

* fix(mobile): preserve credential cleanup authority

* fix(mobile): make host cleanup crash-safe
2026-08-05 21:06:15 -07:00
Brennan Benson 81e7a2ddf7
fix(terminal): recover live panes after renderer restart (STA-3536) (#12776)
* fix(terminal): stop transient probe blips from erroring restored panes (STA-3536)

terminal_pane_owner_unverified fired for every restored pane whenever one
liveness probe answer went missing: a cold-start daemon draining an attach
stampede misses the 2s getSize deadline, and a wedged superseded daemon
(protocol upgrades leave them running) turns every unmapped fan-out probe
null forever.

- probePtyOwners now skips legacy daemons whose startup inventory listing
  succeeded: fresh sessions never route to them, so they provably don't own
  an unmapped id and one wedged zombie can't poison every pane's verdict.
- attachStablePaneOwner retries the probe over a short backoff ladder before
  surfacing unverified, so a single missed deadline resolves to a verdict.
- The renderer replaces the raw error code with actionable copy.

* fix(terminal): stop retrying definitive owner probes

* fix(terminal): recover live panes after renderer restart

* refactor(terminal): share owner resolution abort guard
2026-08-05 20:52:22 -07:00
Jinwoo Hong ff01fad4ad
fix(runtime): stop a stalled tick from forgiving banked missed probes (#12841)
Post-merge review of #12790 demonstrated a real leak: the resume-from-pause
pardon cleared banked misses outright, so a host whose sweep stalls once every
three ticks reset the budget forever and a dead socket was never reaped. The
reviewer ran 300 sweeps against a permanently dead peer with a >1.5x gap every
third tick and observed zero terminate() calls.

Pre-#12790 that required a stall on *every* tick; the counter widened the
pathological window 3x, and the failure mode is permanent non-reaping — the
MAX_WS_CONNECTIONS leak the reaper exists to prevent.

A stalled tick now charges no miss, which is all the original rationale needed
(the client had no chance to answer that probe), but no longer forgives the
misses already banked. A live client still clears its own count by answering
the probe that is still sent on the stalled tick.

The tolerance test's pause case is rewritten to assert the new contract rather
than the old forgive-everything one, and a new test pins the leak directly: a
host stalling every third tick must still reap a dead socket.

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 20:28:10 -07:00
Brennan Benson b70e33764f
Revert #12658: show PR status on active and done workspaces too (#12825)
Stacked on the #12793 revert. Widens the passive-identity set from
{inactive} back to {active, done, inactive}, so the PR/check glyph
returns to the left status lane for workspaces that are actively being
worked, not just idle ones.

Tradeoff, deliberate: #12658 was not purely a regression. It also fixed
#8813, where an active workspace with branch identity and no PR showed
the grey branch glyph instead of the emerald Active dot. This revert
reintroduces that, and removes its e2e guard.

The left lane holds one glyph, so activity, branch identity, and review
status cannot all be shown. This picks review status.
2026-08-05 19:40:06 -07:00