* Skip no-op state writes and bound save postponement in persistence
orca-data.json (1.6MB live) was fully rewritten (pretty-print + tmp +
rename) on every debounced save even when the state content had not
changed - measured 3 rewrites/min (~5MB/min) on an idle production
instance, and a sync-flush storm of identical multi-MB writes at every
warm start via persistPtyBinding re-binds.
- Content-hash guard in both writers: a save whose plaintext state hash
matches the last successful write skips serialize+write+rename
entirely. Hashing plaintext (not the payload) because encrypt() uses
a random IV per call. Safe under flushOrThrow's durability contract:
a matching hash means the file already holds exactly this state.
- Debounce 300ms -> 1s trailing with a 5s max-wait. The old timer reset
on every mutation with no bound, so sustained sub-interval mutation
bursts could postpone the write indefinitely; now staleness is capped
at 5s while bursts coalesce.
Co-authored-by: Orca <help@stably.ai>
* Guard the state-hash against sync-flush interleaving mid-rename
From adversarial review: an async writer that had already passed its
generation check and dispatched the rename could have a sync flush
interleave during the await, write fresher state, and record its hash -
the async continuation then clobbered lastWrittenStateHash with a value
describing content NOT on disk, making later saves (including the quit
flush) silently skip. Re-check writeGeneration before recording.
Twin edge: a hash-matching sync flush could skip its write while a
stale dispatched rename lands afterwards, leaving stale content
unchallenged. flushOrThrow now forces the sync write whenever an async
write chain was in flight at entry, restoring the sync-write-last disk
ordering.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Refresh AI Vault session list when the window regains focus
Sessions started after the panel mounted never appeared until a manual
refresh, since the hook only scanned on mount and scope changes. Listen
for window focus and visibilitychange (to visible) while the panel is
mounted and trigger a non-force re-scan, letting the main process's 15s
scan cache rate-limit rapid focus flips. The manual refresh button keeps
its force (cache-bypassing) behavior.
Co-authored-by: Orca <help@stably.ai>
* Keep AI Vault refocus refresh render-free on cache hits
Refocus refreshes now run as background: the loading flag stays down (no
spinner flash on every alt-tab back), and when the main process replays
the cached snapshot (same scope key + scannedAt) the state updates are
skipped entirely so nothing re-renders. Fresh scans and the manual force
refresh apply results exactly as before.
Co-authored-by: Orca <help@stably.ai>
* Bypass AI Vault scan cache on panel entry and refocus, throttled
Non-force refreshes were served the 15s-old cached snapshot, so a
session started right before re-entering the panel or refocusing the
window still didn't appear — only the manual force refresh showed it.
Panel entry and refocus now request a force scan, throttled in module
scope to one forced scan per 5s (surviving panel remounts), so rapid
tab/focus flips still resolve from the main-process cache. Manual force
refreshes count against the throttle to avoid back-to-back full scans.
Co-authored-by: Orca <help@stably.ai>
* Deliver AI Vault refocus via main-process signal; calm in-app triggers
Renderer DOM focus/visibility events never fire on macOS app activation
(verified live: document.hasFocus() stays true and no focus/blur event
lands when the window loses/gains OS focus), so the refocus refresh was
inert. Main now broadcasts browser-window-focus to the renderer as
aiVault:windowFocused and the hook subscribes to that instead.
Sessions started inside Orca (window never blurs) get their own trigger
from agent hooks: re-scan only when an unseen provider session id
appears in agent status. State transitions and message pings on known
sessions are deliberately ignored — keying on them made the panel churn
on every AI message. Event-driven rescans that land inside the 5s
throttle window defer to one trailing scan instead of being dropped.
Verified end-to-end against a dev instance: a headless claude session
appeared in the panel on OS-level refocus and another on panel
re-entry, without the manual refresh button.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Support Claude weekly Fable usage meter
* Reference Claude weekly usage research
* Add distinct Claude Fable weekly meter
* Tighten Claude Fable usage parsing
* Bound hot-terminal full-snapshot checkpoints with a per-session cooldown
A streaming session (build logs, yes) re-triggered a full multi-MB
checkpoint.json tmp+rename on every 5s tick via pending-buffer overflow
or the 5MB log cap - hundreds of MB/min of disk writes from one busy
terminal, and the dominant Orca-generated filesystem event volume.
Cap/overflow-triggered full snapshots now honor a 45s per-session
cooldown: while cooling down the session's checkpoint work is deferred
entirely (no takePendingOutput RPC, no increment appends - appending
past a dropped range would leave a hole in the log, so the on-disk
state stays a consistent, merely stale, prefix) and the session stays
dirty so the timer retries until the cooldown expires. Final/teardown
checkpoints (quit, sleep, clean disconnect) bypass the cooldown, and a
session's first-ever snapshot is never delayed, so cold-restore
fidelity changes only for hot sessions mid-stream: at most 45s of
tail scrollback staleness after a hard crash.
Co-authored-by: Orca <help@stably.ai>
* Harden checkpoint cooldown against crash-boundary and reuse edges
From adversarial review of the cooldown commit:
- Warm reattach by an adapter that was not already managing the session
now forces a full-snapshot re-anchor: the previous adapter may have
drained daemon records it never persisted (deferred hot-session tick),
and appending past that unknown drain point would put a seq gap in the
log, which the restore reader rejects wholesale. This also closes the
pre-existing (previously ~ms-wide) drain-vs-persist crash window.
- Cold-restore re-anchor clears any cooldown inherited from the
pre-crash generation (daemon respawn within one adapter), so the
revived session's first snapshot is never deferred.
- Exit-event routing clears lastFullCheckpointAt (reused ptyIds must not
inherit a dead session's cooldown); fanoutSyntheticExits now clears
sessionsNeedingFullCheckpoint for consistency.
- Backward wall-clock jumps count as expired instead of extending the
deferral.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Branch switches and commits made inside terminals now surface through push
signals instead of waiting for the 30s terminal-only fallback poll:
- The git-common watch now also covers the primary checkout's HEAD,
packed-refs, and index (a few stat calls per tick; no new native watchers).
- worktrees:changed events nudge the active worktree's coalesced git status
runner (visibility-gated, 3s floor).
- OSC 133;D command completion dispatches a window event that nudges the
same runner for the active worktree.
Co-authored-by: Orca <help@stably.ai>
* Promote stale agent targets when live titles prove they are sendable
- Extract centralized `detectAgentSendTitleStatus` helper to determine if an agent is ready or needs permission based on pane and tab titles.
- Prevent stale hook-backed status rows from disabling active targets when fresh live titles and PTYs prove they are sendable.
- Fallback to the tab's launch agent type when status-backed targets have unknown agent types.
- Add comprehensive test coverage for promoting stale pane targets and preserving permission blocks.
* Prevent split panes from borrowing stale tab titles
Introduce `resolveRuntimePaneTitleLeafResolution` to track whether a
tab has any reported runtime pane titles. Use this to ensure that a split
pane does not fall back to the overall tab title when another pane in the
same tab already has active title evidence, avoiding stale target status.
* Resolve single pane title if tab layout lacks root
When a tab layout does not have a root, we can only attribute a single
reported pane title. This ensures that any pane title still suppresses
the stale tab-title fallback.
* Cut fseventsd load: fix macOS exclusion cap, drop wide always-on FSEvents streams
- @parcel/watcher maps plain-path ignores to FSEventStreamSetExclusionPaths,
but macOS caps exclusions at 8 per stream and fails closed. Orca passed 9,
silently disabling ALL daemon-side exclusions: fseventsd delivered every
node_modules/.git event to each per-worktree explorer stream (measured 16x
client CPU). Cap plain paths at 8 (extras demoted to userspace globs) via a
shared filesystem-watcher-ignore module, also unifying the runtime
file-watcher host list so identical roots share one native stream.
- worktree-base-directory-watcher subscribed recursive FSEvents streams over
every repo's ENTIRE workspace root (all worktrees) and whole common .git
(objects included) with glob-only ignores (zero daemon-side exclusion),
just to observe a few shallow paths. Replace with a 2s readdir/stat poller
(zero fseventsd clients, measured 70x less delivery CPU); SSH targets keep
provider.watch unchanged.
- serve-sim state watcher held an app-lifetime fs.watch on $TMPDIR (the
noisiest dir on the system); its existing 250ms existence poll makes the
parent watch redundant, so drop it.
Co-authored-by: Orca <help@stably.ai>
* Eliminate base-poller idle cost: mtime-gated ticks + narrow macOS worktree-metadata stream
- base targets: tick is now one stat of each gate dir (root + nested
containers); the readdir + marker fan-out runs only when a gate dir
changed, with a 30s ungated backstop and a bounded pending-marker
recheck list. Idle tick 600us -> 1.1us at 146 worktrees.
- git-common targets on macOS: replace the 2s entry sweep with one narrow
native stream on <common>/.git/worktrees (tiny, rare-churn tree): zero
idle syscalls and external worktree add/remove/HEAD detection back to
sub-second. Re-arms via existence poll across git worktree prune
deleting/recreating the dir. Win/Linux keep the listing poll (no
fseventsd there; a Windows dir handle could block prune).
- split git-common logic into worktree-git-common-watch.ts (max-lines).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(ssh): add ControlMaster multiplexing for system SSH transport
System SSH transport spawns a new OpenSSH process per exec command
(platform detect, relay install check, node resolution, relay launch,
socket probe). Each process pays the full SSH handshake cost — ~9s on
Uber devpods — making a typical relay connect take 54s+ and reliably
exceeding the 15s startup reconnect budget.
Add SSH ControlMaster multiplexing via a per-target socket in
$TMPDIR/orca-ssh-ctl/<hash>.sock. The first command establishes the
master; subsequent commands reuse it at ~100ms per exec instead of ~9s.
ControlPersist=300 keeps the master alive after commands exit so rapid
reconnects (e.g. on tab focus) also benefit. Windows is excluded since
OpenSSH's ControlMaster support there is limited.
* fix(ssh): address ControlMaster key collision and directory permission risks
- Use target.id in the socket key so distinct SSH targets can never
collide even when configHost/port/user happen to match
- Switch from SHA1 to SHA256 and extend hash slice from 12 to 16 chars
- Stat the control-socket directory after mkdirSync to reject pre-existing
dirs that are symlinks, foreign-owned, or have group/other write bits
(mkdirSync mode is ignored on pre-existing dirs)
- Update two tests that used exact spawn-arg arrays; replace with
ordering assertions (forward flags before --) that stay correct
regardless of which extra ControlMaster options are injected
* fix(ssh): bind ControlPath identity to route and reject symlinked ctl dir
Fold proxyCommand/jumpHost/identity fields into the ControlPath hash so a
target whose route is edited no longer reuses a still-alive master built on
the old route. Switch the control-socket dir check from statSync to lstatSync
so a planted symlink fails the directory validation outright.
* test(ssh): drop tautological argv re-assertion in spawn checks
The toHaveBeenCalledWith re-passed the args array extracted from the same
mock call, making that argument position always pass. argv content is
already verified by the index-ordering assertions above; use expect.any(Array)
so the spawn check only claims what it actually verifies (binary path, stdio).
* fix(ssh): harden system ssh connection reuse
Co-authored-by: Orca <help@stably.ai>
* test(ssh): isolate control socket runtime dir
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Test <test@example.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix: detect valid repos when git rev-parse can't confirm
isGitRepo() has depended entirely on a successful `git rev-parse`
since 18ed7b27d, with catch blocks that collapse every failure into
"not a git repository". When that subprocess fails for a reason
unrelated to repo-ness — a transient spawn / git-shim hiccup in the
packaged app, main-process resource pressure, or a config-level error —
a real repository is silently downgraded to a plain folder. The folder
scanner already tolerates this via a `.git` marker, so the scan reports
"git_repo" but the subsequent addRepo throws, producing the spurious
"Open as Folder" prompt for a valid repo.
Keep `git rev-parse` as the authoritative positive signal, but on any
non-positive result fall back to a validated `.git` marker instead of
returning false: `.git` dir must contain HEAD, a `.git` file must point
at a gitdir, and bare roots need HEAD + objects/ + refs/. A garbage
`.git` file and an empty `.git/` are still rejected, preserving the
validation 18ed7b27d added. Logs a warning when recovery via the marker
happens so the underlying probe failure stays visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address review feedback on repo detection
- isGitRepo: warn at most once per session when recovering a repo via
the .git marker, so a broken-git scan can't flood main-process logs.
- repo-detection test: delete PATH instead of assigning "undefined" when
it was originally unset, avoiding a corrupted PATH for later tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: harden git repo marker fallback
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix: reordered default columns in kanban board
* Introduce dedicated flag to repair reversed default workspace statuses
- Add `_workspaceStatusesReorderedDefaultRepaired` to decouple the
one-shot repair from the initial status order migration.
- Ensure the repair runs for users who saved the reversed default
payload (with "Done" on the left) during a short-lived broken build.
- Support both "Completed" and "Done" labels when identifying default
status shapes to migrate or repair.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Resolve and open file links from native chat messages
- Add `onLinkClick` and `allowFileUriLinks` props to `CommentMarkdown`
to support intercepting links and enabling file URI navigation.
- Resolve relative paths, line/column suffixes, and file URIs from
native chat messages against the active worktree context.
- Handle clicked links in `NativeChatView` by opening the target file in
the editor (or system default via Shift+click).
- Refactor markdown renderers and parse utilities into standalone,
reusable modules to share between terminal and chat components.
* Fix file link resolution and click event propagation
Ensure file links open correctly by falling back to repo-scoped
worktrees if a known worktree lacks a path. Also, stop propagation
on markdown anchor clicks to prevent triggering outer card click
handlers.
Previously, the terminal pane would always perform a clear screen on PTY
attach before replaying the eager buffer. This cleared restored scrollback
unconditionally, even when there was no active terminal content to replay
(e.g., when the buffer was empty or only contained window titles and
agent status metadata).
Now, eager buffers are parsed to determine if they contain display-affecting
content. The attach-time clear is skipped for empty or metadata-only
replays, preserving the restored scrollback history. Trailing partial
control sequences are also trimmed to prevent swallowing live terminal output.
* Add terminal SSH reconnect overlay
* Publish returned SSH connection state to store on reconnect
Write the returned connection state immediately into the renderer store
to ensure the deferred terminal reattach can resume without waiting for
the state-change IPC to land.
Also translate SSH reconnect overlay strings in the non-English locales
and update the overlay visibility condition to use isVisible.
* Delete SSH terminal reconnect empty pane design document
Remove the design document detailing the SSH terminal reconnect empty pane
overlay problem, design, and data flow, as it is no longer needed.
* Add keyboard shortcut to toggle the Quick Commands menu
- New `tab.openQuickCommandsMenu` keybinding action (no default binding)
- TabBarQuickCommandsMenu listens for the binding and toggles open/closed
- Scoped to the active tab group naturally since the component only mounts when its group is focused
* Show keyboard shortcut in Quick Commands menu trigger tooltip
* Add tests
* expand tests
* Expand keyboard toggle to call handleOpenChange and skip repeated keys
- Replace `setMenuOpen` toggle with `handleOpenChange(!menuOpen)` so closing
via keyboard runs the same reset logic (query, focus frame, value override)
- Guard against key-repeat events to prevent rapid toggling on held key
- Wrap `handleOpenChange` in `useCallback` so it's stable enough to include
in the `useEffect` dependency array without causing spurious re-registrations
- Update tests to reflect that re-running the effect between presses is
required for the close path, and add a repeat-event test
* Add docstring to withShortcutHint func
* review: harden quick commands menu shortcut
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>