Commit Graph
5843 Commits
Author SHA1 Message Date
Jinwoo-HandOrca 87fbc4482c Add terminal performance initiative plan
Working plan for the orca-performance branch: verified architecture
findings, workstreams (baselines, #7153 validation, term-speed-2 revival
with merge-scout numbers, stall fixes, flow control, rig extensions,
utilityProcess router, telemetry), benchmark protocol, sequencing, and
baseline-relative success criteria.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 21:06:09 -04:00
Jinwoo-H 122811b3bc Merge PR #7153 (combined #7150 terminal freeze/memory fixes + #7139 cooperative drain) into orca-performance for extended testing 2026-07-02 20:53:08 -04:00
Jinwoo-HandOrca 4254580aa5 Add cross-terminal pipeline benchmark (DSR-fenced throughput + latency probe)
Run inside any terminal (Orca pane, iTerm2, Ghostty, Terminal.app, VS Code)
to measure its full byte path. DSR round-trip latency at idle and under a
paced agent-TUI load, plus fenced throughput over four deterministic
fixtures. The DSR fence forces 'all bytes parsed' before the clock stops so
xterm.js-class ingest queues can't flatter the result.

First piece of the terminal performance initiative's measurement rig.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 20:52:58 -04:00
Neil 8e8a08ac7c fix: supplement Claude Fable usage from CLI (#7167) 2026-07-02 17:45:15 -07:00
Jinjing 2fff91e289 docs: update Android APK link to 0.0.21 (#7163) 2026-07-02 17:06:01 -07:00
Brennan BensonandNeil 99bc693cd5 Fix Codex config paths in managed runtime home (#7157)
* Fix mirrored Codex relative config paths

Orca mirrors ~/.codex/config.toml into a managed CODEX_HOME before launching Codex. Relative path-valued Codex settings were then resolved from the runtime home instead of the user's real Codex home, which made config loading fail in Orca while the same CLI worked in a normal terminal. Rewrite known relative path settings to absolute paths rooted at the system Codex home while preserving runtime-owned trust sections.

* Dedupe Codex TOML line scanner and include path rewrite in CLI tsconfig

* Harden Codex config path rewrite and cover managed account homes

- Track multiline arrays in the shared TOML line scanner so array lines
  are never mistaken for table headers or path keys
- Escape control characters and reject lone-surrogate unicode escapes so
  the rewritten runtime config always stays valid TOML
- Extend the rewrite allowlist with profiles.* file settings and
  debug.config_lockfile.* (both can abort Codex config loading)
- Rewrite relative paths when mirroring the canonical config into
  managed account homes (codex login CODEX_HOMEs), anchoring WSL
  accounts to the Linux-side ~/.codex with posix join semantics

---------

Co-authored-by: Neil <neil@stably.ai>
2026-07-02 17:01:41 -07:00
gatsby74 5da41d4ed6 [codex] Add file explorer open in terminal
Add an Open in Terminal action for file-explorer directories and preserve terminal startup cwd through restore/session paths.
2026-07-02 16:47:38 -07:00
github-actions[bot] 025448f06b release: v1.4.120-rc.2 v1.4.120-rc.2 2026-07-02 23:43:43 +00:00
Brennan BensonandOrca ee8bdf1aac Notarize serve-sim camera dylibs in macOS releases (#7077)
Co-authored-by: Orca <help@stably.ai>
2026-07-02 16:42:56 -07:00
Jinwoo-H 24e620624a Merge branch 'Jinwoo-H/performance-improvement-discord' into Jinwoo-H/test-7150-plus-7139 2026-07-02 19:40:58 -04:00
Jinwoo-HandOrca 5a33edd6ef Extract breadcrumb recording into a collection-safe leaf module
Playwright loads spec imports at collection time, and e2e specs import
terminal-module constants (e.g. terminal-attention.spec.ts pulls
POST_REPLAY_MODE_RESET from layout-serialization, whose chain reaches
replay-guard). The breadcrumb import added to the terminal modules made
that chain reach crash-diagnostics.ts, whose top-level import.meta.hot
and webview-registry import crash Playwright's transform
("ReferenceError: exports is not defined in ES module scope") — every
e2e shard failed at collection before running a single test.

Move recordRendererCrashBreadcrumb into crash-breadcrumb-recorder.ts
(type-only imports, no import.meta) and point the terminal modules and
their test mocks at it; crash-diagnostics re-exports for existing
callers. Full e2e suite collects again (262 tests / 94 files); unit
suites, typecheck, lint green. No runtime behavior change.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 19:40:43 -04:00
Jinwoo-H 0ad23938d5 Merge remote-tracking branch 'origin/main' into Jinwoo-H/test-7150-plus-7139 2026-07-02 18:42:03 -04:00
Eren Çakar 801effa68e fix(claude): use Git Bash-safe hook fast path
Fixes #7116.

Use a forward-slash bare .cmd path for Claude/OpenClaude Windows hooks when the managed script path is Git-Bash-safe, avoiding the per-hook PowerShell startup overhead in the common case. Unsafe paths with spaces or shell metacharacters continue to use the encoded PowerShell launcher from #6078.

Keep Codex on its cmd.exe-safe fast path and leave the other agent integrations on the hardened encoded launcher.

Co-authored-by: Eren Çakar <hey@erencakar.com>
2026-07-02 15:42:01 -07:00
Jinwoo Hong 2ce9314acb Improve Windows terminal performance: retain WebGL contexts, warm first ConPTY (#7085) 2026-07-02 18:41:23 -04:00
Jinwoo-H 0dde645aac Merge remote-tracking branch 'origin/main' into Jinwoo-H/test-7150-plus-7139 2026-07-02 18:39:28 -04:00
github-actions[bot] ce997b001d release: v1.4.120-rc.0 [rc-slot:2026-07-02-15] v1.4.120-rc.0 2026-07-02 22:35:04 +00:00
Jinwoo-H fa107e21e5 Merge branch 'pr-7139-head' into Jinwoo-H/test-7150-plus-7139 2026-07-02 18:29:18 -04:00
github-actions[bot] 4782b8d3bf release: v1.4.119 v1.4.119 2026-07-02 22:21:07 +00:00
Jinwoo-HandOrca 046caa95b0 Scale output backlog caps with the scrollback setting and breadcrumb drops
The 2 MB pending-output caps were flat, which risked dropping lines a
50k-row scrollback user would have retained. Both caps (main pendingData
and the renderer output queue) now derive from one shared policy:
max(2 MB, scrollbackRows x 120 chars) — 2 MB at the 5k default, 6 MB at
the 50k max. The main side reads the setting live via getSettings; the
renderer scheduler is configured where the terminal lifecycle already
reads the scrollback setting.

Every drop now records a rate-limited crash breadcrumb with dropped and
cap sizes (terminal_output_backlog_dropped in the renderer,
terminal_pending_output_dropped in main — no pty ids, session ids can
embed workspace paths). Field drop frequency and size decide whether the
cap constants need raising, replacing theory with data (#2836, #7017).

Backlog skip notices are now cap-agnostic since the limit varies.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 18:20:11 -04:00
Jinwoo-HandOrca bdf798b439 Make replay-guard stall release probe-certified instead of time-based
The previous stall watchdog blindly released the input guard after 10s.
If a replay were genuinely still parsing on a starved machine, that
early release could leak xterm's auto-replies into the shell — and into
agent TUIs, where a leaked ESC reads as the user pressing Escape.

Replace the blind release with a probe: when a completion looks
overdue, enqueue an empty write behind the replay. xterm parses writes
in order, so every outcome is provably safe:
- probe parses after the replay completion ran: normal release already
  happened; probe is a no-op.
- probe parses but the replay completion never ran: all replay bytes
  have parsed, no further auto-replies can exist — the completion was
  genuinely lost. Release + breadcrumb.
- probe never parses (bounded wait): the pipeline is wedged, and a dead
  parser can never emit auto-replies, so releasing cannot leak input.
  Release + breadcrumb naming the pane as needing recovery.
While the probe is pending — a slow-but-alive replay — the guard now
HOLDS instead of releasing early; that case is pinned by a regression
test.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 17:52:46 -04:00
Jinwoo-HandOrca 92abd8cfa7 Cap unbounded terminal output buffers in main and the foreground queue
Field evidence (Discord #performance / #2836): renderer memory climbs to
~1.5 GB and terminals freeze; a force reload does not help until memory
recovers. Two unbounded buffers matched that shape:

- Main-process pendingData grew by string concatenation without bound
  while the renderer could not receive (frozen, starved, mid-reload) —
  main-heap bloat a renderer reload cannot clear. Now capped at 2 MB per
  PTY: past the cap the buffered bytes are dropped and the entry stays
  O(1) until the renderer ACKs again, then a droppedOutput sentinel is
  delivered and the pane repaints from the authoritative main-owned
  buffer snapshot (existing hidden-output restore path) instead of
  continuing a stream with a silent gap.
- The renderer output scheduler capped only hidden-pane backlogs; the
  foreground path could queue a visible pane's flood without bound when
  the drain could not keep up. The 2 MB cap now applies to every
  foreground enqueue branch too, with a foreground-specific skip notice.

Verified: new main-side cap test (starve → flood → sentinel → normal
flow resumes), renderer sentinel-to-snapshot-restore test, two
foreground scheduler cap tests; full pty/terminal-pane/pane-manager
suites (1981 tests) and typecheck pass.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 17:52:46 -04:00
Jinwoo-HandOrca 2c860af9ff Harden xterm write pipeline against sync-throw wedge that freezes panes
A synchronous exception escaping xterm's WriteBuffer loop permanently
wedges that terminal: _innerWrite has no try/catch around the parse
action or the write-completion callback, the tail re-schedule never
runs, and write() only re-arms on an empty buffer. The pane stops
rendering and, if a replay was in flight, the replay guard latches and
pty-connection's onData silently eats every keystroke — matching the
field reports (Discord #performance, issue #2836: content visible,
shell alive, daemon output.log flat). Both vectors verified against
vendored xterm 6.1.0-beta.287 in xterm-write-buffer-stall.repro.test.ts.

Three layers of defense:
- Guard every write-completion callback Orca hands xterm at the two
  choke points (writeForegroundTerminalChunk, writeBackgroundTerminalChunk),
  with settle and onParsed guarded separately so a WebGL/renderer
  failure during viewport settle cannot starve the replay-guard release.
- Guard all throwing-capable custom parser handlers (DA1, OSC 10/11,
  CSI ?h/?l mode reports, OSC 52 clipboard, OSC 7 cwd), degrading a
  throw to "not handled" — same escape class as
  terminal-link-provider-guard.ts.
- Replay-guard watchdog: each engagement releases exactly once, from
  xterm's completion or a 10s watchdog, so a lost completion (wedged
  pipeline, disposed-terminal race) cannot latch the guard on a live
  pane; replayIntoTerminalAsync resolves on either path so restore
  chains cannot hang. Force-releases record a crash breadcrumb.

All guard trips record rate-capped crash breadcrumbs, so the next field
occurrence names the throwing stack instead of failing silently.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 17:52:46 -04:00
Jinwoo-HandOrca e293581e79 Add frozen-terminal repro harness and silent-drop regression tests
Investigation harness for the frozen-terminal reports (Discord
#performance, issue #2836): pane shows content, shell alive, daemon
output.log flat while typing.

- e2e: renderer crash -> auto-reload recovery and three restart/restore
  shapes (live daemon, SIGSTOP-wedged daemon, daemon killed between
  launches), each probing input at both drop layers. Post-crash phases
  drive the renderer from the main process because a crashed target
  severs Playwright's CDP session even though the app recovers.
- e2e helpers: layer-discriminating probes (direct pty.write vs
  transport input, plus pty:listSessions ownership-rebuild revival).
- unit repro: vendored xterm 6.1.0-beta.287 WriteBuffer permanently
  wedges when a sync throw escapes a write-completion callback or a
  custom parser handler (xterm-write-buffer-stall.repro.test.ts).
- unit repros for both silent input-drop layers: main drops writes for
  a live PTY once ptyOwnership loses the id (revived by listSessions),
  and the renderer transport stays unbound after a failed connect.
- pty.test.ts: unregister every leaked SSH provider id in afterEach so
  module-level provider state cannot leak across tests.

Co-authored-by: Orca <help@stably.ai>
2026-07-02 17:52:46 -04:00
github-actions[bot] 2d4b46b44f release: v1.4.119-rc.0 v1.4.119-rc.0 2026-07-02 21:51:38 +00:00
Brennan Benson e44daf37a3 Match main terminal mirror character widths to the renderer (#7148) 2026-07-02 14:50:15 -07:00
Brandon Barker e6ca3087c5 perf(ssh): resolve node path concurrently with remote install state (#6952)
Run independent SSH relay bootstrap probes concurrently when the connection can safely support overlapping execs. Preserve the old sequential path for system SSH without reusable ControlMaster and for remotes that reject concurrent session channels.
2026-07-02 13:55:36 -07:00
github-actions[bot] 98a78f1e0c release: v1.4.118 v1.4.118 2026-07-02 20:12:17 +00:00
Neil 15a151961e Keep terminals responsive under agent output load 2026-07-02 13:02:34 -07:00
github-actions[bot] a81629e862 release: v1.4.118-rc.5 v1.4.118-rc.5 2026-07-02 19:35:52 +00:00
Brennan BensonandNeil 49a8e80526 Temporarily bypass Windows inner signature gate (#7137)
Co-authored-by: Neil <neil@stably.ai>
2026-07-02 12:34:35 -07:00
github-actions[bot] 6bca68ae09 release: v1.4.118-rc.3 v1.4.118-rc.3 2026-07-02 19:13:04 +00:00
Brennan BensonandNeil d2d3e90c96 Fix Windows inner signature PowerShell path (#7136)
Co-authored-by: Neil <neil@stably.ai>
2026-07-02 12:11:58 -07:00
Brennan BensonandOrca 186f6c9e3d Fix stale terminal frames on worktree return: clear alt screen on snapshot restore + WebGL reveal hardening (#7133)
Co-authored-by: Orca <help@stably.ai>
2026-07-02 12:11:27 -07:00
github-actions[bot] 568a869195 Update README downloads badge 2026-07-02 18:48:53 +00:00
github-actions[bot] 33fda893e9 release: v1.4.118-rc.2 v1.4.118-rc.2 2026-07-02 18:43:30 +00:00
Brennan BensonandNeil fc17c57b6b Fix Windows SignPath PSGallery preflight (#7135)
* Verify Windows app executable signing

* Isolate Windows signing verifier tests

* Handle direct Windows installer extraction

* Fix SignPath PowerShell gallery preflight

---------

Co-authored-by: Neil <neil@stably.ai>
2026-07-02 11:42:44 -07:00
Brennan Benson 4c03924618 Show Git-created worktrees in external discovery (#7078) 2026-07-02 11:24:07 -07:00
github-actions[bot] 1ac6de1ccb release: v1.4.118-rc.1 v1.4.118-rc.1 2026-07-02 18:01:05 +00:00
Brennan BensonandNeil 85cec26773 Prevent unsigned Windows app releases (#6806)
* Verify Windows app executable signing

* Isolate Windows signing verifier tests

* Handle direct Windows installer extraction

---------

Co-authored-by: Neil <neil@stably.ai>
2026-07-02 10:54:19 -07:00
github-actions[bot] dbf80864fe release: v1.4.118-rc.0 [rc-slot:2026-07-02-03] v1.4.118-rc.0 2026-07-02 10:58:11 +00:00
NeilandOrca ad13532ab1 Garbage-collect stale worktreeMeta for externally-deleted worktrees (#7107)
* 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>
mobile-android-v0.0.21
2026-07-02 03:12:26 -07:00
NeilandOrca 3ba3630150 Move githubCache out of the durable state file into a quit-time sidecar (#7101)
* 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>
2026-07-02 02:51:09 -07:00
Jinjing e3f99a7472 Bump mobile app.json to 0.0.20 (#7100) 2026-07-02 02:42:52 -07:00
aa1744e30e Fix cursor agent ime anchor (#7061)
* 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>
2026-07-02 02:41:07 -07:00
Neil 443d6efb50 Tune worktree card drag targets (#7095) 2026-07-02 02:34:10 -07:00
WONGILandJinjing 4f0a141951 Fix mobile terminal Korean IME composition on Android (#7011)
* 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>
2026-07-02 02:31:19 -07:00
Neil e59c04122d Fix Activity thread options trigger ref loop (#7096) 2026-07-02 02:17:38 -07:00
NeilandOrca b51dd80252 Skip no-op state writes and bound save postponement in persistence (#7092)
* 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>
2026-07-02 01:56:25 -07:00
JinjingandOrca 6142ec1a06 Refresh AI Vault session list on window refocus (#7075)
* 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>
2026-07-02 01:45:03 -07:00
Neil 366746ad2b Support Claude weekly Fable usage meter (#7079)
* Support Claude weekly Fable usage meter

* Reference Claude weekly usage research

* Add distinct Claude Fable weekly meter

* Tighten Claude Fable usage parsing
2026-07-02 01:34:44 -07:00