* Verify Windows app executable signing
* Isolate Windows signing verifier tests
* Handle direct Windows installer extraction
---------
Co-authored-by: Neil <neil@stably.ai>
* Garbage-collect stale worktreeMeta for externally-deleted worktrees at load
worktreeMeta rows are minted for every worktree Orca ever discovers but
were only deleted by Orca-initiated removal flows. Worktrees deleted
outside Orca (git worktree remove, rm -rf, agent scripts) purge renderer
session state without ever removing the meta, so the map grew
monotonically - 318 of 503 entries (63%, ~110KB) on the reference
machine pointed at long-deleted paths. These entries never reach the
renderer; they only cost disk bytes, parse time, and memory.
GC at load is deliberately narrow: local-host entries only (SSH/runtime
repos, remote meta.hostId, and WSL UNC paths are skipped - a local
existsSync would falsely condemn remote paths), only when the checkout
path no longer exists, only after a 30-day idle grace (preserving
pushTarget cleanup for recently-vanished worktrees and quick
recreations), and never when the entry lacks timestamps to prove
idleness. Lineage rows cascade like removeWorktreeMeta.
Co-authored-by: Orca <help@stably.ai>
* Fix GC false positives: folder-workspace instances, WSL paths on Windows
From adversarial review of the GC commit:
- Folder-project workspace instances are keyed repoId::path::workspace:
<uuid>; splitting on the first :: made the existence probe test a
path-with-suffix that never exists, deleting idle folder workspaces -
whose meta IS the workspace record - and cascading into terminal-
history deletion. Instance keys are now skipped entirely.
- On Windows, WSL linked worktrees carry Linux-style paths from git
porcelain that a Windows existsSync cannot probe; skip non-Windows
paths on win32.
- Idle-grace check now runs before existsSync (skips the stat fan-out
for active entries and shrinks the slow-NFS startup tail), and a
null worktreeMeta map in the file is normalized instead of throwing
outside the parse guard.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Move githubCache out of the durable state file into a quit-time sidecar
Every PR/issue poll restamps fetchedAt even when nothing changed, which
defeated the content-hash write guard by construction: ~100KB of
refetchable 5-min-TTL cache rewrote the whole 1.6MB orca-data.json once
per refresh cycle - the last remaining non-user-driven writer.
The cache is now memory-only during the session (setGitHubCache no
longer schedules a save; the durable payload and state hash both omit
it) and is snapshotted best-effort to orca-github-cache.json at flush
(quit/reload), so PR/issue badges still paint instantly on next launch.
A legacy in-file cache is used as the seed and stripped by the next
durable write; downgrades just recreate the key, which the next upgrade
strips again. Sidecar loss costs nothing - the data is refetchable.
Co-authored-by: Orca <help@stably.ai>
* Reject null/array pr-issue shapes when reading the github cache sidecar
A corrupt-but-valid-JSON sidecar ({"pr":null}) would seed
state.githubCache.pr = null and crash mobile worktree.ps PR grouping
until the renderer's first cache save repaired it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: correct IME cursor after terminal reattach
* fix: stabilize Cursor Agent IME anchor
* Fix Cursor Agent reattach IME detection
* Harden live-agent reattach detection and always restore cursor visibility
- POST_REPLAY_LIVE_AGENT_REATTACH_RESET now includes ?25h so a false-positive
agent signal (or dead TUI leftovers) can never leave a shell with a
permanently hidden cursor; only focus reporting (?1004h) is preserved.
- Replace the broad getAgentLabel token match (fires on titles like
'ssh devin@host') with an exact cursor-agent native-title check.
- Anchor the replay-payload screen signal on the LAST 'Cursor Agent' header so
scrollback from an earlier finished run does not classify the current screen.
- Make the payload-signal flag replay-shape-aware: incremental
(clearBeforeReplay:false) frames can only add evidence; full-screen replays
remain authoritative.
Co-authored-by: Orca <help@stably.ai>
* Fix anchor scan direction and drop non-decaying launch metadata from liveness
- Scan the visible screen bottom-up in resolveCursorAgentImeAnchor so a
transcript line containing an arrow (e.g. a rename diff) cannot hijack the
IME anchor away from the input box.
- Remove getAuthoritativePaneAgent() from hasLiveAgentReattachSignal:
tab.launchAgent never decays after the agent exits, so it would preserve
?1004h and inject focus-in into the shell left behind by a dead agent.
- Clear a stale replay-payload agent signal on an authoritative empty clearing
frame.
Co-authored-by: Orca <help@stably.ai>
* Veto scrollback-only Cursor Agent signal against the parsed viewport
The replay-byte screen signal also matches a dead run's screen sitting in
scrollback (daemon snapshots serialize full scrollback; the relay buffer is
never cleared). After xterm parses the replay, confirm a payload-only signal
against the visible screen via resolveCursorAgentImeAnchor; when the viewport
shows a plain shell, drop focus reporting (?1004l) and skip the focus-in so a
bare shell never inherits stale focus modes or receives stray focus bytes.
Co-authored-by: Orca <help@stably.ai>
* Make the reattach viewport veto shape-based and latest-frame-only
- parsedViewportShowsCursorAgentScreen now checks the screen shape (header +
input row) via viewportShowsCursorAgentScreen instead of requiring the
parked-cursor precondition, so a live agent whose cursor sits at the caret
is not downgraded.
- Add a replay-signal generation counter so a stale post-parse callback stands
down when a newer replay frame has updated the signal.
Co-authored-by: Orca <help@stably.ai>
* Require the parked cursor in the reattach viewport confirmation
A dead cursor-agent screen can still be painted in the viewport with the
shell prompt below it, which passes a shape-only check and would preserve
?1004h and inject a focus-in into the shell. Confirm a payload-only signal
with resolveCursorAgentImeAnchor (shape + parked cursor): a live agent that
needs the focus-in is by definition parked, and an unparked live agent only
falls back to the pre-fix reattach behavior.
Co-authored-by: Orca <help@stably.ai>
* Complete window stub in pty-connection tests for command-finished event
Main's onCommandFinished now dispatches a window CustomEvent synchronously,
but the node-env connectPanePty window stub omits dispatchEvent. These tests
only passed when another test file in the same vitest worker happened to leave
a window with dispatchEvent; this branch's added test files shift sharding and
expose the gap. Add dispatchEvent/addEventListener/removeEventListener to the
stub so the command-finished path is covered deterministically.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* Fix Korean IME composition by deferring live terminal preedit
The mobile terminal capture field previously sent and cleared every TextInput change, which can break Hangul composition on Android keyboards. Introduce a small commit model and extracted live-input hook so composed text is flushed deliberately while ASCII remains immediate.
Constraint: React Native TextInput has no portable composition event for this path; the fix uses a bounded commit delay for likely IME text.
Rejected: Native-module IME integration | unnecessary for the confirmed JS dispatch/clear failure and higher maintenance risk.
Confidence: high
Scope-risk: moderate
Directive: Keep terminal.send payload shape and buffered command input unchanged; do not claim physical Samsung Keyboard QA without device evidence.
Tested: cd mobile && pnpm exec vitest run src/terminal/terminal-live-text-commit.test.ts src/terminal/terminal-live-input.test.ts src/terminal/terminal-text-input-normalization.test.ts src/terminal/terminal-keyboard-type.test.ts --reporter=verbose
Tested: cd mobile && pnpm exec tsc --noEmit
Tested: cd mobile && pnpm exec oxlint src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx
Not-tested: Physical Galaxy Fold7/Samsung Keyboard and Android emulator/Gboard QA were unavailable; device probes recorded no attached Android device.
* Preserve pending Korean IME text before mobile accessory controls
Accessory keys share the same pending live-input commit gate as TextInput keypress and submit paths, so control bytes cannot race ahead of composed Hangul.
Constraint: React Native mobile input does not expose portable composition events for Samsung/Gboard IME paths.
Rejected: Let accessory buttons keep sending directly | Direct sends can drop pending Hangul before Tab/Esc/Enter/Backspace reaches the PTY.
Confidence: high
Scope-risk: narrow
Directive: Keep all terminal control-byte paths behind the pending live-input flush/local-edit decision before sending to the PTY.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts src/terminal/use-terminal-live-accessory-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx; git diff --cached --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA is still external-device only.
* Prevent stale IME timer flushes after mobile terminal teardown
Pending live-input timers now clear on hook unmount, and accessory Delete documents why it stays local without trimming pending IME text.
Constraint: React Native TextInput lacks a portable composition lifecycle, so pending IME text is guarded by a bounded timer that must not survive screen teardown.
Rejected: Use clearPendingLiveInputCommit during unmount | it would also touch React state/native props during teardown when only timer/ref cleanup is required.
Confidence: high
Scope-risk: narrow
Directive: Any delayed terminal input commit must have an owner-lifecycle cleanup path before sending to the PTY.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec vitest run src/terminal/terminal-live-text-commit.test.ts --reporter=verbose; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/use-terminal-live-input-commit.ts; git diff --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA remains unavailable in this environment.
* Use semantic accessory edits for mobile IME commits
Accessory Backspace/Delete now carry semantic local-edit intent from built-in keys instead of inferring intent from raw bytes, and submit handling is reconnected to the pure submit-sequence model.
Constraint: Custom terminal accessory keys may produce the same bytes as built-ins but should still flush pending IME text before sending rather than being silently treated as hidden-input edits.
Rejected: Classify local accessory edits by raw bytes | That couples future custom controls to current built-in byte encodings.
Confidence: high
Scope-risk: narrow
Directive: Keep semantic input intent separate from terminal byte payloads when pending IME text is present.
Tested: pnpm --dir mobile test; pnpm --dir mobile lint; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile exec oxfmt --check src/terminal/terminal-live-text-commit.ts src/terminal/terminal-live-text-commit.test.ts src/terminal/use-terminal-live-input-commit.ts src/terminal/use-terminal-live-accessory-input-commit.ts app/h/[hostId]/session/[worktreeId].tsx; git diff --check
Not-tested: Physical Galaxy Fold7 Samsung keyboard manual QA remains unavailable in this environment.
* Respect IME flush failures before control input
Propagate terminal.send success from pending Korean IME text before sending Enter, Tab, or accessory bytes, while keeping custom no-pending accessory bytes on the original direct path.
Constraint: PR #7011 review required follow-up control bytes only after the pending composed text send actually succeeds.
Rejected: Treating send invocation as success | It can still reject or no-op when RPC state changed.
Confidence: high
Scope-risk: narrow
Directive: Keep pending IME flush paths async-success-aware before adding new terminal control inputs.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; targeted no-excuse clean for mobile/src/terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Serialize mobile IME flushes before live controls
Treat terminal.send as successful only when the RPC response is ok and the runtime send result is accepted, then route all live-input control sends through a shared in-flight pending-flush barrier.
Constraint: PR #7011 review found that resolved RPC promises and per-call sequencing were not enough to prove pending Hangul text reached the PTY before follow-up controls.
Rejected: Only awaiting each flush-then-send call | Repeatable accessory keys and no-pending sends can arrive while the first flush is still in flight.
Confidence: high
Scope-risk: moderate
Directive: Keep future mobile terminal control paths behind the pending-flush barrier whenever IME text may be in flight.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Queue current IME snapshots behind active flushes
Drain the pending snapshot captured by a control action after any already-active terminal send, and make accessory commit handling explicit so raw fallback is not encoded as an inverted boolean.
Constraint: Architecture review found the previous single-slot barrier could wait for an older flush while skipping newly pending Hangul text.
Rejected: Reusing the prior in-flight promise as the current flush result | It proves only an older snapshot, not the current pending buffer.
Confidence: high
Scope-risk: narrow
Directive: New mobile terminal control paths must distinguish allow-raw, handled, and suppress-raw outcomes explicitly.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Preserve accessory raw-send terminal targets
Capture the terminal handle at accessory keypress time and suppress raw fallback if the active live terminal changes while waiting for pending IME flushes.
Constraint: Independent review found raw accessory bytes could retarget to a different terminal after an async IME flush barrier.
Rejected: Re-reading activeHandleRef as the send target after await | It can point at a different terminal than the keypress belonged to.
Confidence: high
Scope-risk: narrow
Directive: Raw accessory fallback must use the keypress-time target and revalidate it after any await.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed files; no-excuse clean for terminal changed files.
Not-tested: Physical Galaxy Fold7 Samsung keyboard; full session file no-excuse audit still reports pre-existing unrelated violations.
* Document accessory flush barrier intent
Make the non-obvious raw accessory wait/suppress behavior explicit so future changes preserve IME-before-control ordering.
Constraint: CodeRabbit requested a why-comment for the send-now accessory branch.
Rejected: Leaving the barrier semantics implicit | The branch can otherwise look like unnecessary async defensive code.
Confidence: high
Scope-risk: narrow
Directive: Keep comments focused on why raw accessory bytes wait behind IME flushes.
Tested: targeted terminal vitest suite; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt check for changed file.
Not-tested: Physical Galaxy Fold7 Samsung keyboard.
* Preserve buffered accessory raw sends
Keep the stale-handle guard focused on the captured active terminal instead of live-input opt-in state, so buffered mode keeps existing accessory key behavior while async live-input waits still cannot retarget to another terminal.
Constraint: Buffered command input behavior must remain unchanged while fixing mobile Korean IME live input ordering.
Rejected: Requiring live-input enabled handles for raw accessory fallback | suppresses valid buffered-mode accessory sends.
Confidence: high
Scope-risk: narrow
Directive: Do not use live-input opt-in state as terminal liveness for raw accessory sends; validate captured target, active terminal tab, connection, and client instead.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Keep Hangul IME text pending until explicit flush
Avoid timer-driven PTY writes for Hangul candidates so paused Korean composition cannot leak intermediate jamo, while preserving the bounded settle timer for non-Hangul IME text. Also keep disabled live-input accessory fallback behind any existing pending flush barrier.
Constraint: React Native TextInput does not expose a portable composition lifecycle on this mobile surface.
Rejected: Fixed 150ms auto-flush for Hangul | can emit ㅎ or 하 if the user pauses mid-composition.
Confidence: high
Scope-risk: narrow
Directive: Treat Hangul candidates as pending until submit/control/accessory flush; do not reintroduce idle timer commits for Hangul without device-level composition evidence.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Gate dictation toast on accepted live send
Honor the async live-input sender contract so the mobile UI reports dictation insertion only after terminal.send is accepted.
Constraint: sendLiveTerminalInput now returns false for stale, disconnected, oversized, or rejected terminal sends.
Rejected: Toasting immediately after dispatch | reports success for sends that never reached the PTY.
Confidence: high
Scope-risk: narrow
Directive: Treat live-input UI success as terminal.send acceptance, not request dispatch.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check app/h/[hostId]/session/[worktreeId].tsx.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Keep accessory edits on Hangul pending path
Make accessory local edits reuse the Hangul-aware defer policy so built-in Backspace/Delete cannot reintroduce timer-driven Hangul PTY writes.
Constraint: Hangul IME candidates must remain pending until explicit submit/control/accessory flush.
Rejected: Reusing the non-Hangul 150ms settle timer for accessory local edits | can leak pending Hangul after Backspace/Delete.
Confidence: high
Scope-risk: narrow
Directive: Any future pending-text reschedule must use getTerminalLiveDeferredTextDelayMs instead of a hardcoded timer.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile terminal/session files; TypeScript no-excuse checker for changed terminal files.
Not-tested: Physical Galaxy Fold7 Samsung Keyboard manual QA and GitHub Actions jobs, blocked by unavailable device and upstream fork workflow approval.
* Prove Hangul live-input hook ordering
Add a direct hook-level regression so Android Korean IME fixes are covered at the orchestration boundary, not only by lower-level helpers.
Constraint: React Native mobile TextInput lacks portable composition lifecycle events in this path.
Rejected: Relying only on helper tests | misses hook-level pending flush and submit ordering.
Confidence: high
Scope-risk: narrow
Directive: Keep Hangul candidates pending until an explicit terminal action flushes them.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; no-excuse on terminal modules
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Keep accessory raw-send tests precise
Remove a duplicate raw-target assertion whose title implied disabled live-input behavior that is covered at the accessory commit boundary instead.
Constraint: Anti-slop cleanup must preserve existing Hangul/accessory behavior and stay within changed terminal tests.
Rejected: Keeping the duplicate disabled-input wording | it tests the same active-terminal predicate as the preceding case.
Confidence: high
Scope-risk: narrow
Directive: Test disabled live-input buffering in the accessory commit layer, not in the raw-target predicate helper.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Explain stale mobile terminal send gates
Document why async IME flush paths re-check terminal/client refs before sending raw bytes or reporting live-send success.
Constraint: CodeRabbit review requested short why comments for non-obvious stale-send safety gates.
Rejected: Leaving the gates undocumented | future edits could remove the stale-target suppression contract.
Confidence: high
Scope-risk: narrow
Directive: Keep async terminal sends guarded by current client, active handle, tab type, and connection state.
Tested: pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; pnpm --dir mobile exec oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Run mobile IME hook tests through effects
Move the Hangul live-input hook regression from server rendering to react-test-renderer so effect cleanup and unmount timer cancellation are exercised.
Constraint: @testing-library/react-native imports React Native's Flow entry under this Vitest setup, so the narrow effect-running renderer is the compatible test surface.
Rejected: Keeping renderToString | it never runs useEffect cleanup and missed the pending timer cleanup path.
Rejected: Adding @testing-library/react-native directly | it failed before tests with React Native Flow syntax under the current Vitest transform.
Confidence: high
Scope-risk: narrow
Directive: Hook-level IME tests must use a renderer that runs effects when asserting pending flush cleanup.
Tested: vitest targeted terminal tests; pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* Keep hook lifecycle tests quiet
Suppress only the react-test-renderer deprecation warning around the effect-running hook harness so real console errors still surface.
Constraint: CodeRabbit flagged React 19 renderer warning noise; @testing-library/react-native remains incompatible with the current Vitest/RN Flow transform path.
Rejected: Global console silencing | it would hide unrelated test failures.
Confidence: high
Scope-risk: narrow
Directive: Keep the renderer warning suppression scoped to this hook harness and pass all other console errors through.
Tested: vitest targeted terminal tests; pnpm --dir mobile test; pnpm --dir mobile exec tsc --noEmit; pnpm --dir mobile lint; oxfmt --check changed mobile files; terminal no-excuse checker
Not-tested: Physical Galaxy Fold Samsung Keyboard manual QA is not available in this environment.
* fix: flush pending mobile IME input before external sends
* fix: guard terminal command finished event dispatch
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* 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>