Commit Graph
7532 Commits
Author SHA1 Message Date
github-actions[bot] ef55429f3d release: v1.4.162-rc.0 v1.4.162-rc.0 2026-07-29 08:50:00 +00:00
OrcaWin 3240c26bce fix(sidebar): contain nested agent metadata (#11336) 2026-07-29 01:47:52 -07:00
NeilandOrca 3a80fbe162 Revert terminal rendering changes from #10692, #10794, #10871, and #10907 (#11338)
* Revert "fix(terminal): avoid flash while restoring parked terminals (#10871)"

This reverts commit 5a6a9e0b28.

Reverted for terminal rendering regressions (flashing, lost content).
Conflict resolution preserves the forwardRef signature from #10433 and
drops the parked-presentation gating #11016 fed with its effective set.

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

* Revert "fix(terminal): limit pre-paint WebGL resume to macOS (#10794)" and "fix(terminal): stop switch bold flash and Windows lag (#10692)"

This reverts commits 4681edb520 and
8f5a45401f.

#10794 was itself a partial revert of #10692, so both are reverted
together: the Windows retained-WebGL LRU and the macOS pre-paint
(layout-phase) visibility transition that survived it. Terminal
visibility resume returns to passive disposal and recreation on every
platform, and the WebGL context ceiling returns to a flat 128.

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

* Revert "fix(terminal): release an abandoned synchronized-output frame on reveal (STA-2694) (#10907)"

This reverts commit 97cb32c1cc.

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-29 01:47:20 -07:00
Wooseong KimandOrcaWin 238d3a1ea1 fix(terminal): verify clipboard writes so TUI "Copied" never lies (#10827)
* fix(terminal): verify clipboard writes so TUI "Copied" never lies

Windows/Electron can return from clipboard.writeText without updating the
OS clipboard, so Claude Code / OpenCode OSC 52 copy and terminal selection
copy looked successful while paste stayed empty (#8977, same root as #5611).

Verify standard clipboard writes by reading back after write, surface OSC 52
host write failures with a toast, and route selection copy through a shared
helper that only clears the selection after a confirmed write.

* fix(terminal): harden clipboard write verification

* fix(terminal): contain clipboard failure notifications

* fix(terminal): isolate verified clipboard writes

* fix(terminal): address greptile clipboard verify nits

Drop the dead onWriteFailure pass-through from the coalesced OSC 52
handler so failure toasts stay owned by the microtask path. Cover
multi-line / CRLF identity in write+verify tests, and export the
verification-failed error constant for stable matching.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 01:18:31 -07:00
NeilandOrca fa449bc0ef fix(worktree-palette): stop blanked display names from crashing Cmd+J (#11323)
* fix(worktree-palette): stop blanked display names from crashing Cmd+J

Blanking the "Display Name" field made buildWorktreeMetaUpdates emit
`displayName: undefined` as a present key. The store's `{ ...worktree,
...updates }` spread then erased the live name, so the next palette
keystroke threw "Cannot read properties of undefined (reading
'toLowerCase')" in searchWorktrees (crash a1f81ea1, build 1.4.159).

Fixed at three layers so no single guard is load-bearing:
- Producer: persist the blanking intent as '' instead of undefined, and
  let WorktreeSet accept '' so remote/SSH hosts stop dropping the clear.
- Store: applyWorktreeUpdates and applyDetectedWorktreeUpdates drop
  present-but-undefined keys for fields Worktree declares required.
- Readers: resolveWorktreeDisplayName/resolveWorktreeBranchLabel mirror
  the main-side mergeWorktree fallback (custom -> branch -> folder) for
  all four Cmd+J searches, the checks/review index, and the render site.

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

* test(worktree): assert omitted display name shape

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-29 00:56:26 -07:00
余辉andOrcaWin 4517088c42 fix(gitlab): refresh self-hosted provider detection (#9909)
* fix(gitlab): refresh self-hosted provider detection

* fix(gitlab): preserve auth refresh during host probe

* fix(gitlab): merge refreshed auth hosts linearly

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 00:56:05 -07:00
JinjingandOrca a7c8b8e071 fix(terminal): bound SSH & remote hidden-worktree terminal retention (C1) (#10625)
* fix(terminal): park SSH worktrees like local ones (C1 retention, slice A)

SSH ptys were blanket-excluded from hidden-view parking, so a hidden SSH
worktree retained every pane forever (C1: renderer heap climbs to the V8
ceiling). SSH bytes transit local main — fact-mode watchers already cover
them, and main keeps a headless model served over pty:getMainBufferSnapshot
that the SSH reattach path never consulted.

- isParkRestorableTerminalPty: snapshot-backed OR (SSH + policy); threaded
  through both park verdicts, both selectors, watcher coverage, and the
  watcher start guard. Remote-runtime/fail-open/foreign/null unchanged.
- Parked-SSH reveal paints from main's headless model (dimension-matched,
  ~5k rows) and degrades to the relay 100KiB replay unless the snapshot is a
  non-empty source==='headless' payload — never a blank/stale paint.
- Kill switch: settings.terminalSshViewParking (default on).

DESIGN.md records the approved plan and the H1 magnitude non-claim.

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

* fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B)

Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the
slice-A switch off) had unlimited retention: the parking cap/TTL only ever
saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's
panes forever. Retention is now memory-bounded, not eligibility-bounded.

- terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min
  TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over
  hidden worktrees ordinary parking can never evict; reuses the hot-retain
  ranking so last-active exemption, deterministic ties, and deadline-driven
  rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount
  would fresh-spawn and orphan the live shell).
- Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto
  (darkness for uncoverable tabs is the accepted cost); buffers captured via
  the sleep-flow registry before the unmount render; retention TTL added to
  the recheck deadlines for budget candidates only.
- Verdict stays out of its own effect deps; policy test asserts idempotence
  and time-monotone membership (flip-loop dwell regression).
- Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on).

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

* fix(terminal): demote hidden scrollback for eviction-exempt worktrees (C1, slice C)

The retention budget (slice B) must exempt worktrees holding fail-open or
foreign-worktree ptys — a remount would fresh-spawn and orphan the live
shell — which would leave that class unbounded again. Instead, past the same
45min retention TTL their hidden panes drop to the minimum scrollback tier
(measured: ~19MB -> ~1.3MB V8 heap per 50k-row pane; trimmed history is
gone by design, reveal restores the configured cap for future output).

- terminal-hidden-scrollback-demotion.ts: module-state verdict registry
  (parked-watcher pattern) with content-equality notify damping; applied in
  the existing scrollback-rows effect in use-terminal-pane-lifecycle.
- selectScrollbackDemotedTerminalWorktrees: pure, TTL-gated, time-monotone.
- Retention TTL wakeups now also cover exempt worktrees so demotion fires.
- Kill switch: settings.terminalHiddenScrollbackDemotion (default on).

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

* fix(terminal): paint the SSH model snapshot inline, not via nested coordinator (C1 slice A fix)

applyMainBufferSnapshot runs its own structuralReplayCoordinator.run; calling
it from applyReattachPayload (already inside the coordinator when a relay
replay exists) deadlocks on the coordinator's tail chain. The model paint now
mirrors the daemon-snapshot branch inline (folded scrollback + rehydrate +
screen, dimension-matched, escape tail last) and arms the restored-snapshot
seq baseline so deferred/live chunks the snapshot covers dedupe instead of
double-painting. Also falls through (no early return) so reattachPayloadApplied
still latches. Adds the folder-workspace id parity unit case.

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

* test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1)

Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and
reveal restores marker content at multi-viewport scrollback depth. DESIGN.md
records the as-built deltas (inline paint, force-park shape, last-active
floor) and the residuals so follow-ups aren't lost.

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

* fix(terminal): paint SSH reveal from main's model even when the relay replay is empty (C1 review #1)

A relay restart empties the replay buffer; the reveal previously painted
nothing even when main's headless model held the session. The reattach now
prefetches the model snapshot when no structural replay exists (SSH-shaped
ptys only) and paints it inside the coordinator; emptiness is judged on the
composed payload (scrollbackAnsi + data + pendingEscapeTailAnsi) so an
alt-screen snapshot with an empty screen frame still paints.

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

* fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review #2)

Per the approved contract each slice reverts behind its own switch: slice C
now requires only the master terminalHiddenViewParking plus its own
terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for
demotion candidates even with the budget switch off. No DEFAULT_SETTINGS
entries exist for sibling flags (defaults are the '!== false' optional
pattern), so no explicit defaults are added.

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

* fix(terminal): scope eviction exemption to the tab, not the worktree (C1 review #3)

One eviction-exempt tab (fail-open/foreign pty) previously vetoed force-park
for its whole worktree, pinning co-located remote-runtime tabs forever. The
worktree now force-parks while exempt tabs keep their mounted panes via a
per-tab exclusion mirroring the Activity-portal pattern (legacy watcher sync,
legacy render, and the overlay cold-parking hook). Ordinary parking is
untouched — a worktree with an exempt tab still cannot ordinary-park.
Slice C now also demotes exempt tabs' panes as soon as their worktree
force-parks under the count budget (they are the only panes left mounted).

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

* fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review #4)

The last-active exemption means a single hidden un-parkable worktree never
force-parks — and slice C previously only targeted exempt-tab worktrees, so
its panes held full scrollback forever. Demotion now also covers un-parkable
non-exempt worktrees past the retention TTL that are absent from the
force-parked set (last-active spared, or slice B switched off). Membership
stays time-monotone for fixed inputs; covered by new idempotence/monotone
selector tests.

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

* fix(terminal): keep the hidden clock running through transient background-measure windows (C1 review #5)

Whole-worktree background mounts (browser-automation bootstrap lease, mobile
mounts, agent wakes) open a ~3s self-clearing measure window that previously
deleted hiddenSince — every remount restarted the 30s hysteresis and the
45min retention TTL, so a periodically re-mounted force-parked worktree
never re-parked. The measure window still pauses parking/eviction verdicts
(all selectors skip measuring candidates); only the clock survives, so the
prior verdict resumes as soon as the window closes. Visible and
portal-holding worktrees still reset the clock.

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

* test(terminal): make the SSH park+reveal depth assertion prove the model paint (C1 review #6a)

Pad the session with ~180KB of output after the numbered markers so the
earliest marker falls outside the relay's 100KiB rolling replay buffer while
staying inside main's ~5k-row headless model; asserting marker_1 after
reveal now proves the headless-model paint rather than passing under the
relay fallback.

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

* docs(terminal): rewrite DESIGN.md as the single as-built C1 contract (review #7)

One contract matching the code: status IMPLEMENTED around force-park (not
the unmount proposal), real kill-switch names with coupling + revert
matrices, the true retention-floor formula with measured per-pane and
demotion numbers, an explicit when-OOM-is-still-possible paragraph naming
the H2 pendingSideEffects residual, the applyMainBufferSnapshot deadlock
constraint inside the slice-A section, stable-signal phrasing instead of a
capability latch, fail-open AND foreign-worktree exemption class, verified
cites, and a planned/landed/follow-up test matrix.

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

* fix(terminal): resolve the eviction exemption per pane, not per tab (C1 review #8)

isEvictionExemptTerminalTab read only tab.ptyId — the FIRST leaf's pty —
while the coverage veto that makes a worktree a retention candidate walks
every pane. A split tab whose second leaf held an unrestorable pty therefore
failed coverage (→ force-park target) yet looked exempt-free, so force-park
unmounted it and orphaned the live shell. The exemption now resolves panes
through the same resolveParkedTerminalPaneCandidates, keeping tab.ptyId in
the union for the no-layout/no-capture case.

Also from the same review round:
- force-park's capture passes includeLocalBuffers:false like every other
  shutdownBufferCaptures caller; it was serializing up to 512KB/pane of
  scrollback into the store inside a fix meant to bound renderer heap.
- Terminal.tsx unmount resets the scrollback-demotion registry — module
  state with no reset path, read by a pane effect that runs before the host
  effect that would clear it, so a stale verdict trimmed restore replays.
- memoize watcher coverage per tab within the parking pass; the retention
  candidates re-asked it for every mounted worktree, not just the parked few.

* docs(terminal): drop DESIGN.md — the as-built C1 contract moves to the PR body

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

* fix(terminal): cap the deferred PTY side-effect queue (C1 residual H2)

pendingSideEffects grew without bound under background timer throttling
(~64 drained/s vs hundreds queued/s overnight). Cap at 512 entries with
oldest-first eviction: titles drop (last-wins), a pending bell latches
onto the next survivor, agent-status payloads collapse onto the survivor
keeping the newest 16 (last-wins store state, KB-scale strings).

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

* fix(terminal): carry command-lifecycle facts through parked watchers (C1 follow-up)

Parked fact-mode watchers omitted onCommandFinished/onCommandCode*, so
OSC 133;D and Command Code scrape signals went dark while parked. New
parked-terminal-command-status.ts ports the store-level subset: git-UI
nudge on every command finish, same-turn status-row drop for SSH PTYs
(exact mounted-path parity — the foreground tracker refuses SSH ids),
and the Command Code working seed / 1500ms done settle. Byte mode scans
the same shared parsers for authority-off parity. Local-PTY status drops
stay with the mounted pane: they need pty-connection's process-confirm
ladder to tell a leaked nested-shell 133;D from a real agent exit.

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

* test(terminal): retention-budget force-park e2e with a retentionLimit override (C1 6b)

ORCA_E2E_TERMINAL_RETENTION_LIMIT flows preload → e2e-config →
getTerminalParkingPolicyOverrides (exposeStore-gated, positive-integer
only) so a spec can shrink the force-park budget to 1. The Docker-gated
spec opens two remote worktrees on one relay target (second pre-seeded
remote repo), disables terminalSshViewParking to make both un-parkable,
hides both behind the local context, and proves the older one force-parks
while the last-active exemption spares the newest; re-activating the
evicted worktree restores the marker tail via relay replay.

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

* test(terminal): retention-budget e2e via same-repo remote worktrees (passes docker lane)

The first draft added a second remote repo mid-session, whose pane pty
spawn misroutes to the local daemon with the remote cwd (pre-existing
multi-repo issue, reproducible without any retention override — a seeded
local repo plus one remote repo shows the same misroute). The spec now
budgets across three worktrees of the ONE connected repo, created through
the product createWorktree path (an external git-worktree-add only lands
as a detected worktree needing adoption) and polled through the relay's
transient post-connect reconnect window. Verified green on the local
Docker lane in 20.8s.

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

* fix(terminal): prevent remount thrashing during post-measure cool-down (

Implements the C1 retention contract: preserve worktree `hiddenSinceMs` through a
background-measure window (so TTL/ranking stay honest), but re-park waits for a
full `coldParkDelayMs` cool-down after the measure ends. Without the cool-down,
every ~3s measure lease on a past-deadline worktree thrashes remount/reattach.

Core changes:
- Terminal.tsx: add measure clock (measuringTerminalWorktreeIdsRef) and post-measure
  cool-down tracking (terminalWorktreeParkCooldownUntilRef); gate parking candidates
  until cool-down expires.
- Extract snapshot replay choreography to shared terminal-snapshot-replay-paint.ts
  (used by SSH reattach + daemon restore paths).
- Add SSH model snapshot timeout (750ms) with fallback to relay replay.
- Move cold-park recheck deadline logic to terminal-cold-park-recheck-deadlines.ts;
  add cool-down deadline to scheduling.
- useTerminalTabColdParking: implement matching measure-clock contract with per-tab
  cool-down gate to keep tab deadlines synced with worktree retention clock.
- Add resolveTerminalMountScrollbackRows() to demote new xterms under demoted
  worktrees (pane births during demotion must take the demoted tier at create).
- Add kill switches: terminalSshViewParking, terminalHiddenWorktreeRetentionBudget,
  terminalHiddenScrollbackDemotion.

* fix(terminal): detect Command Code completion in parked mid-turn panes

Seed the byte watcher with in-flight turn state from agent status: the
watcher is recreated per park cycle with no startup command to arm it,
and the banner scrolled away before parking. Also memoize
eviction-exempt checks and use SSH PTY ID builder in tests.

* fix(terminal): flush pending command-code settles on reveal remount

When a parked pane reveals mid-Command Code turn, the new detector
cannot re-observe the already-passed idle composer. Cancelling the settle
leaves the row stranded at 'working', so dispose now flushes the pending
settle instead.

Extract readInFlightCommandCodeTurn to shared space and seed detectors
with in-flight turns so remounts complete mid-flight commands. Also
memoize SSH model probes to prevent double timeouts on reattach.

* fix(terminal): remove scrollback demotion (C1 slice C)

The scrollback demotion feature for eviction-exempt hidden worktrees is no longer needed. Retention budget limits are now sufficient without this additional bound. Remove the terminal-hidden-scrollback-demotion module, the selectScrollbackDemotedTerminalWorktrees function, and related per-pane demotion logic.

* test(terminal): assert bounded probe during stalled reveal

Add assertion to verify that a stalled reveal operation makes exactly one
`getMainBufferSnapshot` call, ensuring retry logic doesn't introduce
redundant probes that would extend the timeout window before relay fallback.

* fix(terminal): implement C1 retention budget for hidden parked worktrees

Addresses OOM regressions in hidden parked terminals by force-evicting
worktrees past a retention budget: at most 12 mounted while hidden, none
past 45 minutes (absolute, not exempted by last-active). Eviction is
least-recently-hidden-first. Exempt tabs (unrestorable local PTYs) keep
their panes to avoid orphaning shells; worktrees are force-parked even
if they contain exempts, and their buffers released elsewhere. SSH/remote
worktrees serialize buffers pre-eviction for reveal; local worktrees keep
daemon snapshots. Command Code's done-settle window is transferred across
park/reveal boundaries so the row cannot strand at 'working'. Model probe
on SSH reattach is scoped to park-reveal only, not ordinary reconnects.
Includes new E2E suite proving the budget actually releases memory.

* memoize eviction-exempt terminal tabs to avoid redundant store reads

Each tab's exemption check re-reads the store and walks the layout tree.
Introduce selectEvictionExemptTerminalTabIds() to resolve all exempt tabs
for a worktree in a single pass, then memoize the result in Terminal.tsx
and useTerminalTabColdParking. This prevents O(n) store reads when checking
exemptions across multiple tabs and ensures the set remains stable across
unrelated re-renders.

* refactor: reformat hidden-worktree retention comments

Reflow to 80-character lines and remove internal ticket references
(C1, C1 slice C).

* fix(lint): split overlay slot and eviction-exempt tabs under max-lines

Static analysis failed because TerminalPaneOverlayLayer (401) and
terminal-parked-tab-watchers (304) exceeded oxlint max-lines. Extract the
slot component and eviction-exempt helpers into dedicated modules.

* test(terminal): stabilize retention budget e2e control arm

Stage un-parkable remote pty ids only after both worktrees are hidden, and
keep re-staging during the control-arm poll so a late updateTabPtyId cannot
flip the decoy back to park-restorable and ordinary-park it before budget
engages.

* test(terminal): pin retention e2e decoy to a mounted pane snapshot

Use the active pane-identity snapshot for the decoy tab instead of all
worktree tabs, and re-assert un-parkable ids after the control-arm hold so
a deferred/empty tab id cannot fail the budget-off mounted-count check.

* fix: memoize terminal eviction exemptions on layout leaf PTYs

Splits add leaf panes to the layout store without changing the tabs
array. A memo keyed only on tabs misses this change, leaving new panes
unexempted for unmount. Include layout leaf PTYs in the exemption memo
key so it recalculates when splits occur or PTYs are re-minted.

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-29 00:47:18 -07:00
OrcaWin e549dd3ef4 fix(agents): make pane retention transfer-aware instead of suppressor-based (#11310) 2026-07-29 00:35:06 -07:00
Neil b4b3bcdb84 perf(vault): look up resume worktrees through the shared index (#11317)
Resolving a session's resume target scanned every worktree in every repo:
two `Object.values(worktreesByRepo).flat().find(...)` calls, which also
allocate a fresh 1124-element array each time, plus an equivalent `some()`
walk. All three run per visible session row, so a panel render repeated
them ~20 times.

`getIndexedWorktreeMap` already exists for this and is WeakMap-cached on
`worktreesByRepo`, so the index is built once per store snapshot rather
than per call. `connection-owner-resolution.ts` already resolves worktrees
this way.

~1.6ms -> ~0.002ms per render pass at 1124 worktrees. Net -4 lines.

Behavior is unchanged: the map dedupes by id, which only diverges from
`find()` when one id appears twice with different objects. Worktree ids are
`repoId::path`, so a duplicate id within a repo array — the documented race
the index was built for — refers to the same worktree.
2026-07-29 00:20:47 -07:00
Neil 6cc579a48c perf(vault): dedupe scope paths by key instead of rescanning (#11314)
* perf(vault): dedupe scope paths by key instead of rescanning

#11303 took the session maps off the workspace-switch path, but scope path
derivation still follows the active worktree and stayed quadratic.

addAiVaultWorkspaceScopePath deduped by re-normalizing every already
accepted path on each insert, so accepting K paths cost O(K^2)
normalize('NFC') calls — ~632k at 1124 workspaces. isAiVaultWorkspaceScopePathClaimed
separately rescanned every live worktree per prior id, and runs twice per
switch via activeWorktreePaths and scopePaths.

- carry a Set of comparison keys alongside the paths, so each insert is one
  normalize plus one Set lookup
- thread that accumulator from the workspace pass into the project pass
  rather than restarting deduplication against a plain array
- build one comparison-path -> worktree id map for the claim check, keeping
  first-writer-wins to match the previous some() short-circuit

deriveAiVaultScopeSessionPaths on a real 1124-workspace profile:
189.7ms -> 0.8ms. Output is unchanged, including ordering: verified against
the previous implementation across 117 scenario/option combinations
covering monorepo and mixed-repo layouts, priors both claimed and
unclaimed, duplicate paths, NFD/NFC, WSL UNC, trailing and doubled
separators, relative and blank paths, and four project-key shapes.

Adds the first test file for this module: scope semantics (priors, claimed
priors, cross-repo rejection, dedupe, NFD/NFC) plus a timing guard.
Verified fail-first — the guard reports 220ms on the previous
implementation. Path length is chosen deliberately, since normalize() cost
scales with it and short synthetic paths understate the old shape.

* fix(vault): make the claim check independent of worktree ordering

Review catch on the first pass: keying claims by comparison path meant a
duplicate path had to pick one owner, and picking the active worktree
masked a real claimant later in the list. Concretely, with the active
worktree also listed at its own prior path, the prior was reported
unclaimed where the previous some() reported it claimed.

Excludes the active worktree while building the set instead of comparing
ids at read time, so any surviving entry is a claim by construction and
ordering cannot decide the result.

Adds a test over four orderings, verified fail-first against the previous
commit. Also adds a timing guard for deriveAiVaultWorkspaceScopePaths,
which the session-scope guard did not cover.

Equivalence rerun against the pre-optimization implementation: 156
scenario/option combinations, identical paths and ordering.
2026-07-29 00:11:04 -07:00
OrcaWin d07931c4c2 fix(mobile): keep host action drawer close stable (#11306) 2026-07-28 23:50:53 -07:00
Neil c5102e1262 test(vault): guard the workspace-switch regression #11303 fixed (#11311)
#11303 removed the O(sessions x roots) path normalization from the session
worktree map, but nothing fails if that shape comes back. Adds the two
checks that were missing, plus the tool that would have caught it.

- timing guard: 1200 worktrees x 400 sessions must build in <150ms.
  Verified fail-first — restoring the pre-#11303 per-session
  buildWorktreeCandidates call takes it to 238ms; it is ~15ms as merged.
- path boundary: '/repo/alpha-sibling' must not be attributed to
  '/repo/alpha'. Hoisting the root normalization out of the loop must not
  degrade containment into a bare startsWith.

Also adds tools/benchmarks/workspace-switch-paint-latency.mjs, which
attaches over CDP and measures first-paint-after-click and max frame gap.
The existing worktree-switch-responsiveness.spec.ts only times the
synchronous click task, which stays ~1ms because the highlight is a direct
DOM mutation — that is why a ~1s stall could ship without tripping a
budget. On the affected build it read maxFrameGap p50=973ms.
2026-07-28 23:45:05 -07:00
Brennan Benson 9cf31bdc00 perf(vault): stop rebuilding session maps on every worktree switch (#11303)
Switching worktrees rebuilt two ~500-entry maps in the Agent Session
History panel because their memo deps included the active repo/worktree,
which the maps never read; the worktree map also rebuilt ~530 path
candidates (and re-normalized every root) per session, ~255k
isPathInsideOrEqual calls per switch.

- Drop activeRepo/activeWorktree from the sessionProjectById memo via
  buildAiVaultSessionProjectById, and activeWorktreeId from
  useAiVaultSessionWorktreeMap; 'current' is now stamped per row at read
  time (withAiVaultCurrentWorktreeStatus), so switches reuse both maps.
- Hoist candidate building out of the per-session loop and precompute a
  normalized-root matcher per candidate, so map rebuilds on data changes
  are O(sessions + roots) normalizations instead of O(sessions x roots).

NFC folding from #10841 is untouched; non-ASCII (NFD/CJK) matching is
covered by new tests. Warm switch with the panel on All/500 drops
425ms -> 125ms on the full-scale rig (cold 491ms -> 193ms); panel-closed
switches are unchanged.
2026-07-28 23:08:55 -07:00
Brennan Benson 1df8aa5605 fix(dashboard): give the agent preview terminal a real pane's keyboard (#11015)
* fix(dashboard): give the agent preview terminal a real pane's keyboard

The dashboard's preview terminal is a bare xterm, not a pane, so it never
ran `resolveTerminalShortcutAction` — its only custom key handler covered
copy/paste and IME. Ctrl+Backspace therefore fell through to xterm's default
`\x08`, which readline binds to backward-delete-char: one character instead
of a word.

Route the preview's keys through the pane's own shortcut policy, so word and
line kills, Option chords, modified Enter, and scrollback chords encode
identically. Pane-scoped verdicts (splits, search, focus) are swallowed rather
than passed to xterm, which would send e.g. Ctrl+Shift+D as a bare Ctrl+D.

The policy needs three things the preview could not see:

- kitty-protocol flags — mirrored locally from the same PTY output stream
- the PTY's execution host — bytes follow the host, not the client OS, so a
  new `DashboardCard.terminalInput` profile is derived in the main renderer
  (the only one holding the store) and relayed to the pop-out
- host terminal options — the ConPTY backend and the kitty withhold now apply

Also brings the emulator itself up to a pane's: Orca's Unicode 11 width shim
(replayed CJK/emoji/ZWJ laid out wrong without it), Windows Ctrl+Alt chord
repair, user font/cursor/line-height/word-separator/sensitivity settings,
ligatures, the TUI wheel multiplier, lazy Arabic shaping, clickable links, and
the IME candidate anchor — extracted from pane-lifecycle so both surfaces share
one implementation.

The in-window drawer built its snapshot from a slice subset, which would have
degraded the new profile to client-OS defaults there; it now reads the full
store non-reactively.

* fix(dashboard): enumerate pane-scoped chords instead of a default case

The switch-exhaustiveness gate rejects a `default` over the shortcut-action
union — it would let a newly added action be swallowed silently instead of
forcing a decision at the preview's boundary.

* fix(dashboard): preserve native shortcuts and PTY host routing

* chore: keep merge scope limited to dashboard

* perf(dashboard): avoid full-store copies for terminal profiles

* fix(dashboard): validate terminal input profiles at IPC boundary

* fix(dashboard): sync preview terminal refs on commit, not during render

react-compiler rejects ref writes in the render pass; every reader is an event handler or a post-await continuation, so a commit-phase sync is equivalent.

* test(dashboard): cover the three seams that relay the host-input profile

Reverting any of them left every suite green: the dialog's terminalInput prop
(the only reader of DashboardCard.terminalInput), the drawer's hand-threaded
store slices, and the pop-out's republish triggers. Each new assertion was
mutation-checked against its source line.

* fix(dashboard): follow the WSL host for a preview terminal's byte routing

The card resolver handed resolveTerminalInputHostPlatform a transport with no
getLocalSessionMetadata, so a WSL pty on a Windows client resolved to win32
while its own pane resolves linux — Shift+Enter would then encode CSI-u where
the pane sends alt-enter. Mirror the pane transport's own gate.

* fix(dashboard): republish on every slice the host-input profile resolves from

The compare set covered 4 of the ~11 slices that decide a card's execution
host, so a change to the rest (folder workspaces, project groups, the runtime
catalog, detected worktrees) never triggered a publish. On a quiet board there
is no later publish to heal from, and the pop-out — which cannot re-derive the
profile — keeps encoding bytes for the host the pty used to run on.

* fix(dashboard): sync preview terminal refs on layout, not on a passive effect

xterm's keydown is a native listener, so React never flushes a passive effect
before it. A just-relayed host profile could therefore miss the next keystroke.

* test(dashboard): pin the preview's replay-vs-live kitty scan

The existing A/B passed either way: a lone CSI > u sets the same flags through
scan and scanReplay. Redeliver the push across a snapshot and its replay so
stack semantics would leave the TUI's single pop on a stale frame.

* fix(dashboard): rebuild the drawer's snapshot on every host-input slice

The in-window drawer read ~12 host slices through getState() while subscribing
to none of them, on the premise that agent activity drives the next rebuild. A
quiet board has no such rebuild: an SSH handshake completing with every agent
idle leaves the preview terminal encoding bytes for the pre-connect host, and
agentStatusEpoch only ticks on live status changes so it never heals. Watch the
same set useDashboardPopoutBridge republishes on — each writer bails out when
nothing changed, so the added deps are far quieter than agentStatusByPaneKey,
which already rebuilds this memo on every status ping.

The existing coverage mounted a second hook, which always recomputes; the new
test re-renders the same hook after changing only sshConnectionStates.

* fix(dashboard): key the preview by the user's terminal shortcut policy

The preview passed 11 of the 12 inputs the pane's policy takes and let the
12th default to orca-first. Under terminal-first a remapped tab.close chord is
meant to yield to the shell — Ctrl+W is a word-kill there — but the preview
kept claiming it as a pane close and swallowed the bytes.

* test(dashboard): pin the host-input profile to card snapshots only

The count path main added in #11042 renders no cards, so it must not pay a
per-pty host resolution on every agent-status tick. Both assertions run against
a card that does have a live pty, so only the gate keeps the profile off.

* refactor(dashboard): extract the board's client-host read

The merge of main's label bounding pushed build-dashboard-snapshot.ts to
302 lines. The client's own platform facts are a distinct concept from the
pty host each card keys against, so they move out rather than earn a
max-lines bypass.
2026-07-28 21:57:41 -07:00
NeilandOrca aeeae53b1f fix(build): stop pnpm -r from crawling the mobile workspace (#11291)
Co-authored-by: Orca <help@stably.ai>
2026-07-28 21:34:55 -07:00
Wooseong KimandOrcaWin adc10cd21a fix(explorer): refresh tree on create/rename with case-tolerant cache keys (#10392)
* fix(explorer): refresh tree on create/rename with case-tolerant cache keys

Windows watchers can emit paths whose casing differs from the worktree
dirCache key, so create events never refreshed. Also apply rename events
immediately by refreshing the parent listing instead of ignoring them.

* fix(explorer): reconcile Windows update-only creates

* fix(explorer): bound watcher update reconciliation

* fix(explorer): index watcher cache paths

* fix(explorer): avoid expanded directory rescan

* fix(explorer): batch watcher subtree purges

* fix(explorer): preserve Windows drive roots

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 20:52:09 -07:00
afbd98d8a4 Support Windows drives in the remote host filesystem picker (#7439)
* Support Windows drives in the remote host filesystem picker

The remote picker was locked to the system drive on Windows hosts: the
breadcrumb root resolved to C:\ and typed drive paths (M:\dev) were
treated as filter text, so projects could only ever be created on C:.

- Server: answer host-root browses ('/') on win32 with the mounted
  drives instead of resolving to C:\.
- Client: recognize drive-anchored input (M:\, M:/, m:) as path mode,
  resolve segments from the normalized drive root, and make
  joinPath/parentPath/breadcrumbs drive-aware. Up from a drive root
  returns to the host root (the drive list).

Fixes #7438

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

* Document why joinDrivePath uses a literal backslash

Review feedback suggested path.win32.join, but the renderer bundle
imports no Node builtins anywhere and runs sandboxed, so path.win32 is
not available here. The backslash targets the remote Windows host
regardless of client OS; say so at the call site.

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

* Complete Windows drive browsing over SSH

* fix remote Windows drive browsing

* fix(ui): key remote breadcrumbs by path

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 20:51:14 -07:00
余辉andOrcaWin 3f53287554 fix(mobile): accept WebSocket pairing addresses (#9912)
* fix(mobile): accept websocket pairing addresses

* fix(mobile): align manual pairing address validation

* docs(mobile): correct custom address grammar comment

* fix(mobile): enforce pairing endpoint size limit

* fix(mobile): reject canonical IPv6 wildcard addresses

* fix(mobile): handle unscannable pairing offers

* fix(mobile): reset custom address dialog on close

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 20:34:26 -07:00
JinHyeok JeongandOrcaWin 6107789c97 Fix WebSocket fallback for reserved Windows ports (#7185)
* Fix WebSocket fallback for reserved ports

* fix(runtime): narrow reserved-port fallback

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 20:28:39 -07:00
Brennan Benson af2972b3a9 fix(mobile): declare happy-dom so terminal-webview tests run standalone (#11238)
mobile/src/terminal/terminal-webview-{tap-routing,init-surface}.test.ts
request the happy-dom vitest environment, but happy-dom was only declared
at the repo root. The mobile suite resolved it by walking up into the root
node_modules, so `cd mobile && pnpm install && pnpm test` fails with
ERR_MODULE_NOT_FOUND and loses those 12 tests unless a root install
happens to be present.
2026-07-28 20:27:42 -07:00
OrcaWinandOrcaWin 76b6c137c6 fix(orchestration): sanitize legacy formatted JSON (#11263)
* fix(orchestration): sanitize legacy formatted JSON

* fix(orchestration): harden legacy message formatting

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 20:16:55 -07:00
d9fec8fd61 fix(updater): report releases still being published (#8914)
* fix(updater): distinguish releases still publishing (#8869)

* fix(updater): preserve verified releases during probe outages (#8869)

* test(updater): expect publishing copy for perf checks (#8869)

* fix(updater): keep transport failures out of publishing copy (#8869)

* fix(updater): preserve channel and feed fallback semantics (#8869)

* test(updater): prove feed and asset failure boundaries (#8869)

* test(updater): cover unavailable manifest probes (#8869)

* fix(updater): fence publishing copy to stable releases (#8869)

* fix(updater): preserve nudge deferral across release channels (#8869)

* fix(updater): retain benign nudge handling for probe outages (#8869)

* test(updater): preserve channel and transport proof fidelity (#8869)

* test(updater): model asset HTTP status in feed fixtures (#8869)

* fix(updater): preserve legacy prerelease probe handling (#8869)

* test(updater): cover unavailable publishing-window nudge retention (#8869)

* test(updater): prove publishing retry and channel cases (#8869)

* fix(updater): preserve truthful readiness states

* fix(updater): type release preflight failures

* fix(updater): keep probe outages truthful

* fix(updater): keep not-ready diagnostics neutral

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 19:58:58 -07:00
余辉andOrcaWin 3e4abef089 fix(settings): keep local WSL settings scoped to the desktop host (#9635)
* fix(settings): scope local WSL settings to the desktop host

* fix(settings): verify local WSL capability ownership

* fix(settings): respect capability host ownership

* fix(settings): isolate paired host capabilities

* fix(settings): key web capabilities to paired host

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 19:58:55 -07:00
9150ac65cb Fix Windows setup sequencing wrapper quoting (#8806)
* fix(setup): correct Windows sequencing wrapper quoting

* test(setup): preserve spaced Windows batch paths

* refactor(setup): dedupe PowerShell encoder, clarify wrapCmd comment

Route the Windows setup-sequencing and Hermes startup planners through the
shared renderer-safe encodePowerShellCommand instead of two verbatim btoa
copies, and make that shared encoder renderer-safe (Buffer is unavailable in
the sandboxed renderer where both planners also run). Reword the wrapCmd
comment so it describes the current single-outer-quote behavior instead of the
old quote-doubling bug.

* test(setup): cover Windows metacharacter paths

* fix(setup): keep Windows runner paths out of cmd source

* test(setup): preserve Windows setup failures

* docs(setup): explain safe cmd path handoff

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 19:58:52 -07:00
c6c6c71196 fix(opencode): use cross-platform data directory (#10362)
* fix(opencode): use cross-platform data directory

* fix(opencode): honor in-memory database override

* fix(opencode): harden database discovery coverage

* test(opencode): reproduce Windows session discovery

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 19:58:49 -07:00
github-actions[bot] c8dba6d72c release: v1.4.160-rc.5 v1.4.160-rc.5 2026-07-29 01:36:20 +00:00
Jinwoo HongandOrcaWin 0660ad9d6e fix(orchestration): reject legacy mail acknowledgment (#11227)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 18:27:52 -07:00
NeilandOrca 5c59c84c7a fix(plugins): close four trust-boundary holes in the plugin system (#11232)
* fix(plugins): close trust-boundary holes in the plugin system

Move five security decisions to their chokepoints rather than leaving them
enumerated at individual call sites.

- Kill-list revocation reaches content packs: PluginContentPackRegistry now
  takes an isKilled predicate and intersects it with any caller-supplied
  approval, so a killed plugin's VM recipes can no longer reach
  spawn(..., { shell: true }) through either reconcile() call site.
- Bound kill-list generatedAt to a 24h future skew at the parse chokepoint.
  A far-future timestamp previously made every genuine later list look
  "older" and disabled revocation permanently, persisted across restarts.
- Protect the whole auto.components.settings.Plugin* translation subtree
  instead of an enumerated prefix list, so language packs cannot forge the
  consent provenance badge or rewrite install-error security copy.
- Resolve manifest panel icons by own-key only; "constructor"/"__proto__"
  previously yielded non-component prototype members that crashed the
  right sidebar to its error boundary.
- Give panel liveness frames a reserved control budget so a panel that
  saturates its action budget can still answer the watchdog.

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

* fix(plugins): keep the kill-list future bound off the cache read path

The schema-level generatedAt bound re-judged the on-disk cache against the
device clock at every launch, so a client whose clock ran behind the last
genuine publication discarded its whole cached kill list and started with
zero revocations. Move the bound to the two fetch chokepoints instead.

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

* fix(plugins): remove the reserved-lane starvation window and the revocation TOCTOU

Review follow-ups on the trust-boundary fixes:

- The reserved liveness lane had a per-window count equal to the ping
  interval, so a panel's own pong-shaped traffic could spend it and drop
  the next genuine reply — reintroducing the starvation the lane exists to
  prevent. The lane is now size-bounded only; rate stays bounded because
  every pong is also charged to the data budget.
- Only schema-valid pongs take the lane now, so near-miss pong-shaped junk
  cannot drain it. readPanelPongId replaces the zod parse on this
  guest-controlled path (a rejected safeParse allocates an issue list, ~90x
  the accepted-path cost) and is pinned to the schema by a parity test.
- Re-read the kill list inside approveAtomically: approvedKeys is snapshotted
  before an awaited verification phase, so a plugin killed during that wait
  could still publish VM recipes and language packs.
- Assert the curated icon resolves to FileText; the old equality also passed
  when both sides fell back to Plug.

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

* fix(plugins): match zod's safe-integer bound in the pong reader

readPanelPongId used Number.isInteger, but zod's .int() rejects anything
above 2**53-1, so pingIds like 1e100 took the reserved lane the schema
would have refused. The parity test never probed that boundary.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:49:20 -07:00
NeilandOrca 3a67186623 fix: stop notification loss, credentialed cache reuse, and clipboard clobber (#11230)
* fix: stop notification loss, credentialed cache reuse, and clipboard clobber

Mobile catch-up (#8591): fetchMissed swallowed the RPC failure while
deliverLive kept advancing and persisting lastDeliveredSeq, so the next
successful catch-up asked from above the abandoned range and the desktop
cut it. Sessions are module-scope, so an unchanged epoch never resets it.
Quarantine the watermark at the last contiguously-delivered seq and hold
it there until some later catch-up actually drains — not just one retry.
A batch cut short by a teardown quarantines at the last event it settled.

Jira attachment cache: currentEpoch summed two independent counters, so a
site at siteEpoch 1 read the same value before and after a global clear.
The mid-flight guard passed and re-inserted credentialed image bytes that
"disconnect all" had just purged — resident for the process lifetime since
pruneExpired has no timer. One monotonic ticker, compared by max.

Web copy fallback: the handler registered in the capture phase, so xterm's
bubble-phase listener overwrote text/plain with the terminal selection
afterwards; served was already true, so the copy reported success. Every
Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA,
PR URL) pasted the terminal selection. Bubble phase with
stopImmediatePropagation. Covers the secure-context retry branch too,
which shares the same helper.

* fix: roll back the persisted watermark on catch-up failure; cover stopImmediatePropagation

Adversarial review of a98d7f4d5d found two gaps.

1. The quarantine clamped only writes made AFTER the failure. getMissedSince
   waits up to 30s, so a live event routinely persists a higher seq while the
   request is still outstanding; that value stayed on disk, and the next launch
   read it back and resumed past the abandoned range -- the original bug,
   reached through a restart. quarantineCatchUpWatermark now re-persists the
   clamped seq, so the stored value never outlives the gap it guards.

2. web-clipboard-copy-terminal-selection's second test registered its "late"
   document handler BEFORE the fallback's, so it lost on registration order
   alone and stopImmediatePropagation was never exercised -- the test passed
   with that line deleted. Bubbling reaches the document before the window, so
   a window-level listener is what actually requires it.

* fix(mobile): mark a notification seen only once its show lands

A pre-marked seen key made a rejected show unrecoverable: the next
catch-up re-fetched the seq and the dedup guard dropped it, and the
first later event to drain the batch lifted the quarantine past it.
Also contains the rejection so it does not escape the un-awaited
'ready'/live handlers as an unhandled rejection.

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

* test(web-clipboard): pin stopImmediatePropagation with a same-target handler

Both existing cases passed with plain stopPropagation, and with the listener
back in the capture phase — neither half of the fix was actually pinned. The
window-level clobber is on a different target, so stopPropagation suppresses
it too. Registering the clobber on the document, ordered after the fallback's
own listener, is the only shape stopPropagation cannot cover.

Addresses the review comment posted after the last commit.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:38:31 -07:00
Brennan Benson 681c4ba458 fix(skills): stop calling the updater's own install a modified copy (#11249)
After a successful headless update the CLI installs source-repo HEAD,
which legitimately runs ahead of any shipped bundle. The scan classified
those bytes 'unrecognized', so the row went amber ('may be modified…
remove it') seconds after our own Update button ran, and the advice
looped: remove + reinstall lands the same newer content.

Scan half: a canonical/alias placement whose observed git tree sha
equals the updater lock's skillFolderHash is the CLI's own install, not
a user edit — reclassify it 'newer-known'. Display half: 'newer-known'
is recognized official content ahead of this build with nothing to fix,
so it no longer marks the copy blocked. Eligibility is deliberately
unchanged: ahead of the bundle means there is nothing this build can
update to, and offering one risks the provably-unperformable update
(#11110) when source HEAD still equals the lock.

Copies whose sha does not match the lock, copies with no lock entry,
same-name copies outside the placements the command writes, and
plugin-cache behavior all stay flagged exactly as before.
2026-07-28 17:27:26 -07:00
NeilandOrca 1d7e7656e3 fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales (#11241)
* fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales

7.1 ui.set rejected whole preference payloads on enum drift. The new
AssertNoMissingKeys guard is key-only, so it could not see that
LegacyWorktreeCardProperty omitted 'cli' (in DEFAULT_WORKTREE_CARD_PROPERTIES)
or that rightSidebarTab omitted 'workspaces'/'pr-checks' and every plugin tab.
UiUpdate is .strict(), so one bad value failed the entire batch and silently
dropped sidebarWidth/groupBy/sortBy/filterRepoIds riding the same debounced
write. Both enums now derive from the shared unions, AssertNoMissingValues
catches value drift by name, and UiUpdate drops an unknown value instead of
rejecting the batch around it. Unknown KEYS still reject.

7.2 The SSH shell-ready fallback moved from first-output to spawn, so a remote
shell needing >1.5s to prompt got the bracketed-paste startup command before
readline armed it, with no recovery afterward. The short deadline now applies
only once output proves the shell is talking; a silent-since-spawn shell gets a
longer budget and still delivers eventually.

7.3 The project picker armed in rank order but rendered in section order, so
with a folder group present the BOTTOM row was armed on open and Enter created
the workspace in the wrong place. Row keys now derive from the same sections
that render. The folders bucket also gains the recent-exclusion guard the
projects bucket has; that duplicate was unreachable, so this is symmetry, not a
live bug fix.

7.4 setBrowserPageZoomLevel now compares before writing, so a pane reasserting
a level the host already holds no longer emits a redundant host-wide
HostZoomMap write. The user-applied level also moved to a module-level map
keyed by page id: the guest webview outlives its React pane, so the pane-local
ref re-seeded from the shared Settings default on every remount and let a later
default retroactively hijack an already-zoomed tab. See PR notes on the part of
this finding that could not be fixed as prescribed.

7.5 A non-null sessionId short-circuited the live-work escape hatch, forcing
'loading' over hook 'working' and rendering an idle pane mid-turn: Send instead
of Stop, no typing indicator, no streaming preview. Status stays 'working'; the
empty-transcript loading SURFACE moves to selectNativeChatViewState, which keeps

7.6 #10770 merged from a base predating #8549, reverting 182-187 translated
strings per locale to English (es 182, ja/ko/zh 187) plus en.json's recipesHelp.
Restored by script, only where the English source is unchanged between the two
shas, so later legitimate edits are preserved: 0 keys added or removed, every
value sourced from 97e4776dfe, and the four other English-source changes since

7.7 Match highlighting indexed by UTF-16 code unit but rendered by code point,
so an emoji-named folder showed marks one glyph late. Cosmetic.

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

* fix(ssh): keep fast startup delivery on the short fallback deadline

The 15s no-output budget added for the shell-ready fallback was applied to
every SSH launch, including 'fast' delivery. Fast delivery waits for no
marker and pastes nothing prompt-sensitive, so it gained a 10x startup
delay for nothing.

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

* fix(rpc): generalize the ui.set value-parity guard to every shared key

Naming worktreeCardProperties and rightSidebarTab left the next field to drift
exactly as unguarded: dropping 'pr-status' from groupBy typechecked clean.
Check the value domain of every shared key instead, against z.input (what a
client may send) rather than z.infer (post-transform).

Also move the pure mergeNativeChatLiveSession suite beside the module it covers;
the hook's test file owns an IO harness and was at the max-lines cap.

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

* fix(i18n): re-apply only the locale strings still reverted at HEAD

#10770 merged from a base predating #8549, so its stale locale copies
overwrote ~185 already-translated strings per locale back to English. A
present catalog value always beats the English translate() fallback, so
those strings render English with nothing to signal the loss.

Since that finding was written, #11205 and other upstream translation
passes independently re-covered most of ja/ko/zh. Replaying #8549's
catalogs wholesale would now overwrite that newer work, so this re-applies
a key ONLY where all of the following hold at origin/main: it was
translated at 97e4776dfe, #10770 reverted it, its English source is
unchanged since, and no upstream commit has touched it since the revert.

  es 182  ja 32  ko 17  zh 55

Everything else is left to upstream. Verified zero upstream translations
reverted: every changed key still matches its #10770 value at main. Keys
upstream deleted are not resurrected, and keys whose English source was
edited since are skipped as legitimate source changes rather than reverts
(this is what keeps zh CPU on #11205's deliberate "CPU" over #8549's
"中央处理器"). Key count and order are unchanged in all five catalogs.

en.json's own recipesHelp was reverted by the same stale base and no
upstream commit has touched it since, so it is restored to match the live
source at EphemeralVmsPane.tsx:252.

* fix(rpc): keep null in the ui.set value-parity guard

NonNullable stripped null as well as undefined, so dropping .nullable()
from a `| null` field passed the guard while still rejecting the batch at
runtime -- the exact drift class the guard exists to catch. Proven: making
visibleWorkspaceHostIds non-nullable typechecked clean before, now errors
by name. Also pins the 15s silent-shell budget so it cannot silently
shrink back toward the short deadline.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:25:05 -07:00
NeilandOrca a721125d06 fix(perf): correct three 07-27 perf regressions (#11234)
* fix(perf): correct three 07-27 perf regressions

Traversal capacity cap no longer scales with worker concurrency
(#11026). retainWorkspaceSpaceScanEntry charged a traversal-wide entry
counter, so N workers each holding a listing multiplied the live charge.
At concurrency 48 a 48x2,100 tree (100,848 entries) hit the 100,000 cap
while 100x1,500 (150,100 entries, 50% more) passed, and scanLocalWorktree
treats the capacity error as terminal, reporting an intact worktree as
"Unavailable" with sizeBytes 0. The cap is now per directory listing --
the only quantity fixed by directory shape -- restoring the invariant
docs/workspace-space-scan-resource-bounds.md already states. Aggregate
live retention stays bounded by the unchanged 64 MiB byte cap.

Note: releasing each entry's charge at dispatch (the originally suggested
fix) was measured and does not help; the peak is set at admission, before
any entry is dispatched.

Repo image icons are no longer fully base64-decoded on every snapshot
publish (#11012). sanitizeRepoIcon reached decodeBase64Prefix, which
sized its buffer to the whole payload to read a 24-byte header, running
synchronously inside ipcMain.handle at a 250 ms throttle. Validation is
now memoized on source+src in a BoundedMap. Measured for 10 icons x
256 KB: 37.34 ms -> 0.67 ms per publish.

One over-long card label no longer discards the entire snapshot (#11012).
isDashboardSnapshot was all-or-nothing and dashboard-popout returned
early with no log while replaying lastSnapshot, so `orca terminal rename
--title "<1025+ chars>"` froze the pop-out board on its last good paint
with nothing surfaced. Labels are truncated at the producer, the
validator drops only the offending card, and both the rejection and the
drop are logged. The bound now lives in the shared snapshot contract so
producer and validator cannot drift.

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

* fix(perf): charge a scan listing's parent path once, not per entry

The 4.1 fix made the entry cap per-listing but left the 64 MiB byte cap
charging parentPath.length for every entry in a listing. Because a
listing's entries all share one parent-path string, that multiplied the
path by the directory's width, so the byte cap measured checkout depth
rather than live heap.

The reported symptom therefore still reproduced at the production default
limits: 48 x 2,100 @ concurrency 48 raised a capacity error once the
worktree path passed ~58 characters, while the same layout at concurrency
1 succeeded. The shipped regression test could not see this because it
passes maxRetainedBytes: Number.MAX_SAFE_INTEGER, disabling the only cap
still in play. Measured at a real 65-char worktree root, 3 of the report's
4 documented layouts still failed.

The parent path is now charged once per listing, with its first entry, so
an empty listing strands no charge. Per-entry overhead is unchanged at
512 B + name, which still dominates the estimate, so the OOM protection
the original PR added is preserved.

Adds a production-default-limits case covering the report's layouts under
a deep root, plus an assertion that a short and a deep root reach the same
verdict -- the path independence docs/workspace-space-scan-resource-bounds.md
requires and which no existing test enforced.

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

* fix(perf): prove the icon cache by decode count, not wall clock

The caching test asserted a per-publish millisecond budget, which failed
on CI at 5.64 ms against a 5 ms ceiling. Any threshold flakes on a loaded
box, so count real sanitizeRepoIcon entries instead: 10 repos x 20
publishes is 200 icon checks against exactly 1 decode. Added cases pin
the cache key (payload and source both re-decode; a cached image verdict
never answers for an emoji) and that a rejection is cached too.

Also drops budget.entries, which the per-listing cap left as a
traversal-wide counter no check reads -- exactly the shape a future
guard could reintroduce the concurrency bug from.

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

* fix(dashboard): bound the project filter label the whole board rides on

#11042 added snapshot-level filterOptions whose project labels are
repo.displayName -- the same unbounded source this PR already bounds for
card.repoName, but one level up where dropping a card cannot recover it.
An over-long project name would fail isDashboardFilterOptions and take
the entire snapshot with it, which is the exact frozen-board failure the
per-card drop was added to end. Workspace-status labels are already
capped at 32 by workspace-statuses.ts, so only projects needed this.

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

* fix(dashboard): disambiguate the repo icon cache key

The memoization key joined `source` and `src` with a space, but the
sanitizer's base64 pattern admits whitespace inside a valid `src`. A
rejected icon can therefore split the same concatenation differently and
inherit an accepted icon's cached verdict, reaching the pop-out's
`<img src>` without ever being sanitized. Length-prefix the source.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-28 17:08:14 -07:00
Brennan Benson 2db02562b6 fix(skills): tell the user how to fix a skill the updater cannot converge (#11248)
Re-lands #11129, which was merged into #11128's branch rather than main and
so never reached main. Content is identical to the reviewed and live-QA'd
head ac5ec5b0b0 (1775d83cf6 + ac5ec5b0b0, minus the intermediate merge).
2026-07-28 16:57:51 -07:00
Brennan Benson bb9ae78136 fix(macos): show TCC notice after first prompt (#11243) 2026-07-28 16:51:59 -07:00
Brennan Benson f790d9cbe8 fix(skills): stop the skill review dialog contradicting the badge that opens it (#11128)
* fix(skills): stop the skill review dialog contradicting the badge that opens it

A skill whose only fault was an edited copy or one Orca could not read turned the
setup-rail badge amber and offered Details — and Details opened a dialog headlined
"All installed Orca skills are up to date." over an empty list. The badge says
something is wrong, the dialog it points at says nothing is.

The grouping only returned skills with an out-of-date copy, so those two states
produced no row and the summary fell through to the all-clear headline. Include a
skill when a copy needs attention as well, using one shared predicate so the badge
and the dialog cannot disagree again. A plugin's own copy of a same-named skill
stays out: that is the vendor's, not the user's drift.

* test(skills): pin that a routine outdated copy raises no attention marker
2026-07-28 16:49:18 -07:00
Brennan Benson 747b241145 feat(main): record main-thread hangs so we can measure them (#10256)
A deadlocked main thread never crashes, so it leaves no crash report and no
artifact — incidence has been unmeasurable (n=1 confirmed, macOS 26.5.1,
FB24004458 / electron#52437). This forks a plain-Node watchdog sibling under
ELECTRON_RUN_AS_NODE that survives the deadlock, listens for a 2s heartbeat,
and after 45s of silence writes a marker to userData. The next launch consumes
it, records a durable crash breadcrumb, and emits a main_thread_hang_detected
telemetry event carrying unresponsive_ms and self_recovered.

Observes only — it never kills or relaunches the parent. A true positive
recovers nothing force-quitting wouldn't, while a false positive would SIGKILL
a live main thread mid-write. self_recovered counts exactly the stalls such a
killer would have gotten wrong, so recovery can be built on evidence if the
field numbers justify it.

macOS-only, packaged-only (ORCA_HANG_WATCHDOG_FORCE=1 to test), with sleep-gap
suppression and idempotent shutdown on will-quit.
2026-07-28 16:43:30 -07:00
Jinwoo HongandOrcaWin a6423d565b fix(macos): prevent stale UI surfaces after wake (#11226)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 16:37:13 -07:00
waryanandClaude 638c3ca5d9 fix(agent-status): prevent ghost sidebar row on completed split-pane detach (#10698)
* fix(agent-status): prevent ghost sidebar row on completed split-pane detach

Detaching a done-state split pane into its own tab migrated the agent
paneKey from oldTab:leaf to newTab:leaf. useRetainedAgentsSync only saw
the old key vanish and, finding no suppressor, resurrected it as an
unclickable duplicate sidebar row (and inflated the worktree count).

Plant a one-shot retention suppressor on the source key during
transferAgentPaneAuthority, but only when the source actually held a
live agent, so a suppressor is never leaked for a pane that had none.

Fixes #10675

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

* fix(agent-status): annotate suppressor record type and condense retention comments

Type the migrated retentionSuppressedPaneKeys as Record<string, true> so a
computed-key `true` isn't widened to boolean, which broke the web typecheck.
Also condense the retention rationale comments per review.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-28 16:26:40 -07:00
Brennan Benson d3681f6306 fix(runtime): surface desktop RPC startup failures (#11037)
* fix(runtime): surface desktop RPC startup failures

* fix(runtime): isolate RPC failure telemetry

* fix(runtime): satisfy the changed-code quality gate and kill vacuous dialog tests

The `no-floating-promises` label span covers the whole `app.whenReady().then()`
callback, so adding lines inside it made a long-standing finding overlap changed
code. `void` is the linter's own suppression; no `.catch()` on purpose.

The startup-failure tests were vacuous: mutation runs showed the wait-for-show
deferral, the destroyed-window guard, the `closed` companion event, listener
cleanup, the cause walk, the cycle guard, and the truncation bound could all be
deleted with every test still green. The "not called yet" assertion ran before
any microtask, so it passed either way.

* test(runtime): de-brittle the desktop RPC-failure source assertions

Anchoring the slice on the full destructure and matching the whole dialog
call expression made an innocuous rename break the test with a cryptic
'expected -1'. Match the shape that is actually the contract instead.

* test(runtime): repair the silently-unbounded desktop startup slice

The desktopEnd anchor comment lost a word in 98b00d3a64, so indexOf returned
-1 and slice(start, -1) covered index.ts to EOF. Moving the dialog call to a
path that never runs at startup still passed. Anchor on code instead, and
assert both bounds so a future reword fails loudly.

* test(runtime): bound the attach anchors in the startup ordering slice

Round 3 bounded the desktop pair but left attachStart/attachEnd unguarded in
the same test: deleting the PTY startup barrier from attachMainWindowServices()
and breaking the rateLimits.attach(window) end anchor still left the case green.

* test(startup): bound the last two unguarded slice anchors in this file

Rounds 3 and 4 fixed the desktop and attach pairs; two instances of the same
class survived in the same file, both proven vacuous by mutation:

- it #3 never bounded readyEnd. Renaming the `pairing:` payload key makes it
  -1, widening readyPayload from 372B to ~52KB. Moving the reconciliation
  status out of the serve-ready payload (its whole point) but leaving it later
  in index.ts then kept all 6 cases green.
- it #2 bounded desktopWindowStart against reconciliationStart rather than
  serveEnd. An earlier `Promise.resolve(openMainWindow())` steals the anchor,
  collapsing desktopStartup to '' while every existing guard still passes, so
  its only assertion — a negative — succeeds against an empty string.

Both mutants now fail. `src/main/ipc/pty-startup-barrier-ordering.test.ts:11`
has the same latent shape; left alone as out of scope for this PR.

* fix(runtime): keep walking the cause chain past an unmapped code

getErrorCode returned the first code it found, so an outer wrapper carrying
an unrecognised code masked a nested EACCES/ENOSPC and classified it unknown.
Only a mapped code ends the walk now; every other input classifies as before.

Unreachable today (writeSecureFile rethrows raw fs errors with .code intact),
but the classifier's job is surviving whatever error shape reaches it.

* fix(runtime): tell the user what to fix, not just to restart

The dialog's only advice was "Restart Orca to try again", which is true for
address_in_use and wrong for the rest: permissions, a full or read-only disk,
and a missing data folder all survive a relaunch, so the user restarted, hit
the same failure and had no next step.

Route the error class we already compute into the copy so each cause names the
thing the user has to change. Guidance and telemetry now derive from the same
classifier, so they cannot drift apart.

* fix(runtime): guide users through long RPC paths

* fix(runtime): avoid false window listener warning

* fix(runtime): guard destroyed window before web contents
2026-07-28 16:05:05 -07:00
Brennan Benson 25da91d653 perf(dashboard): stop re-sending repo icon data URLs on every republish (#11089)
* perf(dashboard): stop re-sending repo icon data URLs on every republish

#11012 put repo icons on the dashboard snapshot keyed by repoId. Image icons
are data URLs capped at MAX_REPO_ICON_DATA_URL_LENGTH (400KB) and every repo
contributing a card ships one, while the snapshot republishes up to 4x/sec
(PUBLISH_THROTTLE_MS = 250) for as long as the pop-out is open. Icons change
about never, so that structured-clones megabytes per second across the window
boundary for bytes the pop-out already has.

Publish the map only when it actually changed, comparing by reference since
icons come off immutable store repo records. The two paths where the pop-out
could be starting from nothing — it opened, or it mounted and asked — still
force a full send, so the retained copy can never be the only one.

The pop-out keeps the last map it was given when a republish omits the field.
An explicitly empty map still clears, so removing an icon works.

repoIconsByRepoId was already optional on DashboardSnapshot and
isDashboardRepoIcons already returns true for undefined, so the main-process
validator needed no change.

* fix(dashboard): keep repo icons in the main-process snapshot cache

The bridge now omits an unchanged repoIconsByRepoId from republishes, so the
cached snapshot main replays to a mounting pop-out could be icon-less, blanking
the board's repo glyphs until the forced publish landed. Carry the last map
into the cache; the forwarded payload is unchanged.

Also covers the forced full sends (open, reopen, snapshot request) that no test
exercised.

* test(dashboard): pin the icon omit on the throttled trailing republish

* fix(dashboard): keep the popout bridge effect off the react-doctor gate

The changed-code quality gate reports react-doctor findings that overlap
added lines, and effect-needs-cleanup spans the whole publish effect — so
this PR's edits inside it turned a pre-existing false positive into a red
static-analysis check. Hoisting the store subscriber leaves the effect
owning one disposable; behaviour is unchanged.

* docs(dashboard): correct why watchSnapshotInputs sits outside the effect

The effect owns four disposables (offOpenChanged, offRequested, the store
unsubscribe, and the trailing timer), not one. State the real reason the
subscribe is hoisted so nobody inlines it back and re-reds the gate.

* test(dashboard): pin that the bridge subscribes only while the pop-out is open

The lazy wiring exists so an enabled-but-closed pop-out costs nothing — a live
subscriber would rebuild a cross-worktree snapshot on unrelated store writes.
Nothing pinned the unsubscribe on close.
2026-07-28 16:00:22 -07:00
Brennan Benson 13c193a00a feat(dashboard): add agent status search board (#11042)
* feat(dashboard): add agent status search board

* fix(dashboard): keep idle controls reachable

* chore: drop merge-only formatting drift

* fix(dashboard): compare sparse subagent snapshots safely

* fix(dashboard): satisfy settings handler lint

* fix(dashboard): address review feedback

* fix(dashboard): complete search and localized status copy

* fix(dashboard): pad active filter row

* fix(dashboard): keep idle control in board settings

* fix(dashboard): source filters from workspace state

* fix(dashboard): clarify PR and MR status filter

* fix(dashboard): preserve review and board parity
2026-07-28 15:51:43 -07:00
Brennan Benson 5753cf6c5c fix(updater): resume background checks after a local build session ends (#11223)
A local-build check (Option+click "Check for Updates" on macOS) pins
activeUpdateSource to 'local' for the rest of the process. The
'update-available' success path never restores it, and
runBackgroundUpdateCheck early-returns on it, so every wake-from-sleep
check, window-focus daily check and nudge poll became a no-op once a
local build reached 'available'. The one-shot automatic timer fired into
that early return and nothing re-armed it, so the scheduling chain died
too and lastUpdateCheckAt froze.

Restoring the source when 'update-available' fires would break the flow
the user just started — the pending download still needs the local feed
and allowDowngrade. Instead the release source is restored when the user
closes the offered card, which main previously never learned about, and
only while status is exactly 'available': downloadUpdate() flips status
to 'downloading' synchronously before it calls into electron-updater, so
this cannot fire once a download is under way.

The automatic timer now re-arms when a check is deferred rather than
launched, so a deferral can no longer end automatic checks for the
process lifetime.
2026-07-28 15:40:31 -07:00
Brennan Benson 3c0cd6069f fix(release): stop packaging plugin authoring examples into app.asar (#11087)
* fix(release): stop packaging plugin authoring examples into app.asar

electron-builder's `files` is an all-negation list, so its default `**/*`
packs anything without an explicit `!` entry. examples/ arrived with the
plugin system in #8549 and never got one, so 1.4.160-rc.3 shipped
examples/plugins/hostile-panel/panel.html — the adversarial fixture the
panel containment tests point at, complete with its fetch-exfiltration
probe — plus hello-orca, inside every user's app.asar. Verified against the
installed 1.4.160-rc.3 artifact, not just the config.

The two orchestration design docs landed at the repo root in the same span
and shipped the same way; fold them into the existing root-doc negation.

Neither has a runtime consumer: bundled plugins ship via extraResources
from resources/plugins/launch/, which is already excluded from the asar
for exactly this reason.

* test(release): assert the examples exclusion through the real file matcher

The added case mapped each negation to a bare top-level token, so it passed
under '!examples/README.md' — a pattern that still ships the whole tree. Drive
app-builder-lib's FileMatcher instead so the assertion matches the test name,
and pin the root anchoring so the negation cannot grow into '!**/examples'.
2026-07-28 15:40:28 -07:00
NeilandBrennan Benson a8126a0a92 fix(macos): explain the TCC prompts, and surface Full Disk Access only to users macOS is prompting (#9756) (#9910)
* fix(macos): add a Full Disk Access nudge to reduce recurring TCC prompts (#9756)

macOS shows the "Orca wants to access other apps' data"
(kTCCServiceSystemPolicyAppData) prompt and it can keep reappearing. The
reappearing loop is not a fixable app bug: it is TCC identity churn — an
unsigned local rebuild mints a new code identity each build, so macOS treats
each as a new app — and Orca's other-app reads are already gated behind opt-in
settings or explicit user actions.

The durable remedy for the population we can help (release users) is Full Disk
Access, a superset macOS grant that stops these prompts for a stable identity.
Surface it with an ambient, dismissable sidebar card that reuses the existing
developer-permissions IPC. macOS-only; probes FDA status at most once per
renderer session (the probe itself reads protected data, so it must not repeat
on focus/remount); "Open System Settings" opens the Full Disk Access pane;
permanent localStorage dismissal.

* fix(macos): stop the FDA nudge promising macOS will stop asking

The card said Full Disk Access makes "macOS stop asking", but the grant
covers this app while terminals are spawned by the detached PTY daemon
(daemon-init.ts forks execPath with ELECTRON_RUN_AS_NODE + detached:true,
reparented to launchd), which macOS treats as its own TCC identity. A user
who followed the card would grant FDA and still be prompted from terminals.
Scope the claim to reducing prompts and name the terminal caveat.

* fix(macos): drop stale focus refreshes in the FDA nudge

refreshFullDiskAccessStatus() applied whichever getStatus() round-trip
resolved last. Rapid blur/focus puts several in flight, so an earlier
pre-grant 'unknown' landing after a newer 'granted' un-hid the card and
also wrote 'unknown' into the module-level session cache, re-nagging a
user who already has Full Disk Access for the rest of the session. The
adjacent FullDiskAccessSetupPrompt already guards this with a refresh
sequence; mirror it here.

Also unmount React roots in afterEach: clearing document.body left them
mounted, leaking each test's window focus listener into later tests.

* test(macos): unmount the StrictMode FDA nudge root between tests

The afterEach unmount added in 5a0f717 only covers roots created through
renderNudge(). The StrictMode probe test builds its own root, so it was
never unmounted and its component stayed live for the rest of the file.
Today that component has no window focus listener, so nothing breaks; add
a CTA click to it and the same contamination 5a0f717 fixed comes back —
the two tests after it see extra getStatus() calls and fail. Register the
root so the fix covers every mount site.

* fix(macos): attribute the FDA prompts to agent activity, not Orca's own reads

The card said the prompts happen "when this copy of Orca reads protected app
data", but Orca's own reads are small and gated; #9756's trigger is agent
find/grep sweeps into ~/Library/Containers, which macOS bills to Orca because
Orca is the responsible process for every terminal child. Blaming Orca reads
as an accusation and hid why FDA works at all — the grant attaches to Orca
rather than to each churning child binary.

Name agents as the trigger, keep the "reduce" hedge and the terminal caveat,
and drop the "this copy of Orca" dev-build hedge that cost a clause. Assert
the causation wording so it can't silently regress.

* fix(macos): explain the TCC prompts on the settings row, drop the sidebar card

The sidebar nudge added in 344d466b was premised on FDA being reachable
"only inside onboarding". It isn't: Settings > macOS Permissions has had a
full-disk-access row all along (searchable), the Setup Guide hosts the same
prompt from both a settings pane and a re-openable modal, and the sidebar
already links to that modal via the "Onboarding checklist" entry. The card
added a fifth affordance to the same sidebar that already had the fourth,
so it bought prominence rather than access - shown to every macOS user
without FDA, most of whom never hit #9756.

Keep the part that was actually new. The settings row still described the
prompts as something projects and worktrees trigger, which is the same
misattribution the card carried: the reads come from the agents Orca runs,
and macOS names Orca only because it is the responsible process for every
terminal child. It also never mentioned that the grant has to cover Orca
Helper, or that the preserved daemon keeps stale TCC state until restart.

Non-English catalogs get the English string as a placeholder; the bootstrap
translators key their cache on the English value, so a changed string is
re-translated on the next run.

* feat(macos): nudge Full Disk Access only after macOS repeatedly prompts

The FDA hint is only worth showing to users macOS is actually prompting.
tccd emits one AUTHREQ_PROMPTING line per consent dialog it displays,
carrying the service and both identities, so a narrow log-stream predicate
detects the real thing without correlating across lines or guessing whether
a dialog appeared. Verified against a captured dialog: the predicate matched
1 line out of 1436 TCC lines in ~28s, because routine preflight checks - the
overwhelming majority of TCC traffic - do not emit it.

Count dialogs where Orca is the responsible process, persist across launches,
and tell the renderer on the third one. The event separates the accessing
binary from the responsible app, which is the crux of #9756, so the toast can
name the tool that triggered it rather than blaming Orca generically. One
toast per user, with a permanent opt-out; it deep-links to the FDA row in
Settings > macOS Permissions rather than restating the guidance.

macOS-only: the watcher no-ops elsewhere, the web client stubs the API, and
the child is killed on before-quit since log stream ignores a closed stdout.

* test(macos): pin the platform so the TCC watcher tests exercise the darwin path

start() is darwin-gated, so on Linux CI it no-opped and the stream/kill
assertions passed vacuously against a watcher that never spawned. Pin
process.platform per the existing convention (shared/secure-file.test.ts),
and cover the gate itself with an explicit non-darwin case.

* fix(macos): start the TCC watcher from app bootstrap, not the window wiring

attachMainWindowServices is called directly by its own unit test, so wiring
initTccPromptNotice there made `vitest src/main/window/` spawn real `log stream`
children that outlived the run - two orphaned watchers were left behind by a
single test session. Only the IPC handler registration stays there; the spawn
moves to the real app bootstrap in index.ts, which tests never execute.

Verified: running the suite that leaked now leaves the watcher count unchanged.

* fix(macos): clarify repeated permission notice

* fix(macos): keep TCC notice lifecycle safe

* fix(macos): retain pending TCC notice delivery

* fix(macos): acknowledge TCC notice delivery

* fix(macos): release failed TCC notice claims

* fix(macos): retry transient TCC notice display

* fix(macos): contain TCC notice IPC failures

* fix(macos): harden TCC notice renderer lifecycle

* fix(macos): contain TCC notice dismissal failures

* test(macos): satisfy promise executor lint

* fix(macos): detect helper-attributed TCC prompts

* fix(macos): align TCC watcher lifecycle and helper identity

* perf(macos): defer TCC log reader until first paint

* fix(macos): recover deferred TCC watcher startup

* fix(macos): recover TCC watcher from deferred quit

* fix(macos): localize recurring file access notice

* fix(macos): preserve TCC watcher and localized guidance

* fix(macos): avoid duplicate TCC watcher recovery

* fix(macos): wait for locale before TCC notice

* perf(macos): isolate TCC notice subscriptions

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-28 15:34:52 -07:00
Jinjing 8b57e6e180 fix(ssh): resync after watcher terminal retry (#10691)
* fix(ssh): resync after watcher terminal retry

* fix(ssh): resync after watcher terminal retry

- Coalesce repeated recovery resyncs within 5s to reduce SSH refreshes
  during link flaps
- Abort in-flight watcher installs when a replacement provider registers,
  preventing duplicate watchers from old and new transports
- Clear resync state when removing watcher snapshots or on provider change
  to prevent stale retry timers

* fix(ssh): resync after watcher terminal retry

Avoid logging spurious warnings when a remote watcher is already closed or
suspended. Move the console.warn call in handleRemoteWatcherTerminalError()
to after the early-return checks. Refactor createSender() in tests to
properly simulate the destroyed event for better coverage of retry-cancellation
behavior.
2026-07-28 15:30:07 -07:00
Brennan Benson 77ac0bd517 fix(codex): keep the stale-pane prompt when two accounts share a label (#11228)
The startup sweep asks main which panes are stale, and main answers by
account id. The renderer then threw that away: it resolved both ids to
labels and let the store's A -> B -> A collapse compare the strings. Two
accounts can share a label — doAddAccount has no duplicate-email check, so
one OpenAI login used in two ChatGPT workspaces gives both the same email,
and a failed roster read collapses every account to 'Codex account'. Either
way the notice was deleted for a pane that really is running under the
account the user switched away from.

The sweep then made it permanent: it marked every stale pane notified,
including the ones whose notice had just been dropped, and a notified pane
is suppressed for the rest of the session. Relaunching cleared the set but
the deletion recurred, so the prompt never came back and the pane kept
running on the other account's auth.json and quota, silently.

Carry the account ids into the notice and decide on them, falling back to
labels only for callers that have none; report which panes were left holding
a notice so a dropped one cannot claim suppression. The prompt also names
the ChatGPT workspace when that is what tells two same-email accounts apart,
which is what the two duplicated getCodexAccountLabel copies now share.
2026-07-28 15:29:57 -07:00
Brennan Benson 50f46889d9 fix(ai-vault): resume a bridged Codex session under the selected account's home (#11224)
* fix(ai-vault): resume a bridged Codex session under the selected account's home

The account session bridge hardlinks every rollout into each per-account
CODEX_HOME, and vault dedup keeps the lexicographically-smallest alias, so
Resume could pin an inline CODEX_HOME naming a peer account — running the
session under that account's auth.json and quota. At resume time the owning
host now substitutes the selected account's home when it holds the same
rollout at the same sessions-relative path, declining on any uncertainty so
resume degrades to today's behavior instead of failing.

* fix(ai-vault): repin dropped sessions without a cwd instead of resuming under the wrong account

The drag payload only carried sessionCwd when session.cwd was truthy, so a
null-cwd codex session dropped onto a pane silently fell back to the prebuilt
command - which pins the wrong account's CODEX_HOME, the exact defect this PR
eliminates on the other resume surfaces.

- Serializer always sends sessionCwd (null when the session has no cwd), so
  absence now only means an older-serializer payload.
- The repin rebuild accepts a null cwd (the builders already omit the cd
  prefix), matching the sidebar Resume/Copy paths which repin regardless of cwd.
- An unrepinnable payload (absent sessionCwd) now fails loudly with guidance
  instead of silently resuming under the wrong account's home.
2026-07-28 15:18:07 -07:00
Jinjing ca5a821600 Stop relaunching creation-time agents on workspace activation (#10647)
* fix(activation): stop relaunching the creation-time agent on workspace activation

Activating a workspace with zero renderable tabs launched the agent it was
created with, unprompted and in approval-bypass mode. Navigation is not consent
to start a process: the same fallback fired from post-delete focus handoff, the
jump palette, keyboard cycling, CLI/relay activation, and notification clicks.

The mechanism was superseded. #1814 added it when relaunching the created agent
*was* the resume feature; #4706 later added real provider-session resume six
lines above and left the fallback in place. What remained fired whenever a
workspace had no renderable tabs -- including when nothing had ever slept -- and
reported itself as `request_kind: 'resume'` while resuming nothing, discarding
any resumable session a plain tab close had already purged.

No caller depends on it. All seven intent-carrying callers pass an explicit
`startup` on the branch where they intend a launch, and every no-startup branch
either declined an agent, already has one running (host `didSpawnStartup`), or
is this same defect arriving over IPC.

Drops the now-orphaned imports, retargets the stale comment in
launch-work-item-direct that cited reopen-relaunch as the reason to persist
`createdWithAgent`, and moves the WSL default-args quoting assertion to
launch-agent-in-new-tab, whose launch path still resolves those args.

Regression tests are revert-sensitive -- all four fail if the fallback returns.

* test(activation): name the relaunch regression tests after what they reach

Three tests were named after scenarios they never invoked, which is the
failure mode that lets a coverage gap read as closed.

- The "host-originated" test's `notifyHostRuntime: false` is inert here: both
  gates resolve through `isWebRuntimeSessionActive`, false with no runtime
  environment seeded, so it was byte-identical to the plain reopen test. It no
  longer claims to cover the host `didSpawnStartup` leg, which lives in main and
  is unreachable from this layer.
- The "post-delete focus handoff" test never deleted anything and never touched
  `prepareActiveWorktreeFocusAfterDelete`. That caller is asserted directly in
  active-worktree-focus-after-delete.test.ts, which locks out any opts.
- The activate/close loop resets state instead of calling `closeTab`, so it does
  not exercise the sleeping-record purge its comment claimed.

Also folds the primary reopen test onto `seedEmptyActivatableWorktree` — the
fixture extracted for exactly that state, which its inline copy had drifted from
by hardcoding a POSIX repo path.

`preflight` is dropped from the launch-work-item-direct comment: the trust
preflight reads the create-time argument (worktree-remote.ts), not the persisted
meta. Removal safety and ownership do read the field and remain accurate.

Renames the ported quoting test to what it pins. Under vitest's node
environment `navigator.userAgent` carries no "Windows", so platform resolution
bails before the WSL branch and the WSL preference is inert — the real coverage
is single-quote escaping of user-configured agentDefaultArgs.

* transfer large terminal history seeds across bounded protocol messages

- Oversized cold-restore snapshots (>1MB) now upload via chunked startHistorySeedTransfer/appendHistorySeedTransfer protocol instead of inline, avoiding NDJSON line-size violations
- Checkpoints automatically trim oldest rows to fit within configured byte limit (200MB) before commit
- Protocol v30 required for chunked transfers; v29 daemons gracefully fall back to renderer-only recovery
- NDJSON encodeNdjson() validates line size and rejects oversized payloads; notifications silently swallow encoding errors

* fix(daemon): drop held output when teardown checkpoint fails to serializ

When a final snapshot checkpoint fails to serialize (returns retryable), the
pending output records must not be appended later—doing so would splice them
over the seq gap left by the failed snapshot, defeating gap detection. Drop
the records and retry the checkpoint instead.

* Bump daemon protocol version to 30

* Bump daemon protocol version to 30
2026-07-28 15:12:44 -07:00
Brennan Benson 930ff96152 fix(skills): stop the scan issue budget evicting a read failure (#11221)
The per-scan issue budget kept an issue only when it explained a candidate
or truncated the walk. Neither set intersects the attention set, so
'io-error' — the sole reason a plugin-cache scan can raise "Needs
attention" — was droppable. Once 16 ordinary issues filled the budget (16
'outside-root' vendor symlinks is an install shape the scan itself
documents as normal), a later read failure was evicted for a generic
'issue-limit' row that raises neither attention nor truncation, and the
dialog headline read "All installed Orca skills are up to date" over a
path that could be hiding a stale copy.

Attention issues now outrank the budget, capped at a small reserve so an
adversarial tree of unreadable folders cannot pin one issue per folder.
2026-07-28 14:41:27 -07:00
Brennan Benson a81f17c189 fix(skills): trust the updater's lock when a run installs content newer than the bundle (#11220)
skills update installs source-repo HEAD, which routinely runs ahead of the
revisions a shipped build bundles. The post-run re-scan hashed that content
'unrecognized' (the registry has never seen it) and the verdict counted it as
a failure — so a clean update reported "The update didn't finish / Updated 0
of N", and Retry repeated the false failure forever because the CLI now
no-ops (lock == source). The 'newer-known' escape hatch never fires: the
generator always points the manifest at the registry's newest snapshot, so no
observed content can hash to a revision newer than the bundle.

The verdict now computes the git tree sha of the observed bytes (a port of
the generator's hashing, verified byte-for-byte against git write-tree and
against every shipped skill's manifest gitTreeSha) and accepts an
unrecognized placement when that sha equals the lock's skillFolderHash: the
lock is the CLI's own record of what it installed, so disk matching lock
means the command did its job — the bundled registry simply has not seen
that revision yet.

Half-written bundles (sha mismatch), unreadable copies, removed skills,
degraded aliases, and outdated copies at the lock hash all still fail.
2026-07-28 14:37:31 -07:00