* ci: share mobile route analysis and scope mobile test runs
* ci: cover mobile web runner process dependencies
* test: verify mobile web selectors through the new runner
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(terminal): recognize a Git Bash launcher by its install layout
* fix(terminal): require git-bash.exe in the launcher install-layout check
* test(terminal): pin each Git Bash launcher install-layout marker as necessary
* perf(history): coalesce tombstone directory rescans
* perf(history): reuse one tombstone listing instead of re-reading per completion
Refilling the 64-slot tombstone removal window used to list the whole
`.pending-delete` directory again, synchronously, after every removal that
finished. A backlog of 1,024 tombstones cost 1,026 listings of a directory
that starts 1,024 entries long, all on the main process thread.
Each history root now keeps the names from its last listing and takes the
next one from memory when a slot frees, listing the directory again only
once that list runs out. The listing moved to `fs.promises.readdir`, so it
no longer blocks the main thread.
This replaces the previous coalescing-on-setImmediate approach, which left
the directory being re-listed once per completion batch and needed a
deferred-roots set, an event-loop turn of latency, and an unref'd immediate
whose freed slots could go unfilled at exit. Holding the names removes all
three.
Two behaviours are kept deliberately:
- Freed slots are offered across roots, so a root displaced at the shared
cap is not stranded until the next startup.
- A listing cannot re-submit a removal that was already under way when it
started, including one that finished before the listing resolved.
Five real-filesystem samples per version, 1,024 tombstone directories each
holding a meta.json, macOS arm64 / Node 24. Medians: directory listings
1,026 to 6, names enumerated 494,348 to 1,077, blocking main-thread time in
this path 820 to 7 ms, total drain 886 to 75 ms. The 886 ms breaks down as
266 ms of `readdirSync`, ~554 ms of per-entry work over those 494k names
and its garbage, and ~70 ms of actual recursive rm, which is the unchanged
floor the remaining 75 ms consists of. Average concurrent removals return
to 62 of 64 from the 46 a deferred refill left idle.
Adds coverage for a listing that fails mid-drain with later completions
still able to recover, and for both roots draining under one shared cap.
The heavy drain tests now run on real timers so real `fs.promises` and
microtask ordering are exercised.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(history): warn when a tombstone root read fails for a reason other than absence
readTombstoneNames swallowed every readdir error, so EACCES or ENOTDIR on the
initial enumeration left tombstones in place with no diagnostic until a later
completion happened to re-read. An absent root stays silent; it is the norm.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf: avoid repeatedly encoding retained VM recipe output
* perf: capture retained VM recipe output as raw bytes in the shared byte buffer
The previous commit added a third byte-retention buffer to the repo. This
replaces it with the one that already existed and removes the remaining
encoding work.
`runRecipeCommand` no longer calls `setEncoding('utf8')` on the child's stdout
and stderr. It keeps the raw `Buffer` chunks and runs one `StringDecoder` per
stream to feed the existing string callbacks, which is exactly how
`setEncoding` is implemented, so callbacks see the same characters at the same
boundaries. With the bytes already in hand the capture encodes nothing: the
4,194,304 bytes the ring still encoded for 4 MiB of output drop to 0, and the
UTF-8 continuation trim collapses from one scan per chunk to a single scan when
the tail is decoded.
Retention is now `GrowingByteBuffer.appendRetainedSuffix`, which had no
production consumer. It gained an O(1) head offset, so `discardPrefix` and
`retainSuffix` mark bytes dead instead of moving the whole tail and `append`
slides or grows only when the head offset runs out of room. Quick Open path
accumulation and the SOCKS handshake buffer get that win too. Without the
offset the per-chunk memmove costs 12.36 ms for 4 MiB; with it, 0.25 ms against
the ring's 0.53 ms and the old per-chunk re-encode's 265.78 ms.
Two behaviour notes. Odd capture limits are clamped once at entry instead of
carrying a per-chunk coercion path no production caller could reach, so an
infinite or NaN cap is now bounded at 1 MiB rather than retaining everything.
And malformed UTF-8 yields a different tail: replacement characters no longer
inflate the byte count, so a malformed tail keeps more of what the recipe
actually wrote.
The encoding-budget assertions no longer spy on `Buffer` itself, where any
unrelated allocation in the same tick could flip them. They count bytes through
the capture's own buffer class and still assert the deterministic oracle: at
most 5 MiB moved for 4 MiB of output, exactly 4 MiB appended, 1 MiB decoded,
and the stored chunks identical to the Buffers the stream delivered.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(vm-recipe): emit Buffers from the doctor stream doubles
Dropping setEncoding('utf8') means stdout and stderr now deliver Buffers, so
the hand-rolled EventEmitter doubles emitting strings threw inside the data
listener — the capture retained nothing and the exit path never settled.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): name the missing unzip when Bun archive extraction cannot start
Extracting the downloaded Bun runtime shells out to `unzip` on POSIX hosts,
which a minimal Debian/Ubuntu install does not ship. runProcess rejects a
missing program with a bare `spawn unzip ENOENT`, which the caller's
non-zero-exit branch never sees, so the operator got an errno instead of a
remedy. Translate that one errno into a message naming the tool and the
ORCA_UNZIP_BIN override.
* fix(ssh): reuse the canonical absence predicate for extractor launch failures
isDefinitiveAbsence is the repo's single errno allowlist for "definitively not
there", and it also covers ENOTDIR — which spawn throws synchronously when a
configured ORCA_UNZIP_BIN has a regular file for a parent. That case previously
escaped as a bare `spawn ENOTDIR` naming no path at all.
Also correct the comment: a deleted working directory is not a second source of
these errnos, because runProcess leaves cwd unset and the child inherits the
parent's without resolving it. Verified on macOS, Linux and Windows.
* perf(mobile): coalesce stalled connection log persistence
* fix(mobile): bound connection-log writes and flush the log before the app suspends
The coalescing pass counted a pending persistence attempt per append and then
burned that whole budget on unblock. With failing storage, 500 appends during a
stall released ~1000 back-to-back `setItem` calls — and slow storage and failing
storage are the same device condition, so the amplification fired in exactly the
scenario the coalescing was for.
The counter is gone. A single `dirtyHosts` flag replaces it: the host's revision
always holds the newest entries, so counting appends bought nothing but writes.
The loop re-reads the revision after each save, so a stall costs the in-flight
snapshot plus one attempt at the newest one, whatever the append count. That also
retires the compound `finally` condition and its unreachable `(… ?? 1) - 1`.
A failed snapshot no longer gets an immediate second `setItem` against a store
that just rejected. It gets one retry after 200 ms, and none at all once a newer
snapshot is queued, because that snapshot already carries the same entries.
`flush()` closes a data-loss gap that predates the coalescing: a write that
failed was only retried by the next append, so when the disconnect was the last
thing to happen the entries explaining it never reached storage. It drains the
in-flight save and makes one more attempt at the newest snapshot, wired to
AppState `background` the way `subscribeConnectionRevivalTriggers` wires resume.
Two revisions tests asserted the retry budget as intended behaviour (6 writes
for 3 appends; 3 for one failure) and now assert one write per snapshot
generation instead. `connection-log-buffer.test.ts` is untouched, including its
requirement that one transient failure self-heals without another append — that
is what the single delayed retry keeps.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(mobile): type the background-flush test mock so the tests ratchet passes
The new test copied `ReturnType<typeof vi.fn>` from
connection-revival-triggers.test.ts, which is grandfathered in the
tests-typecheck baseline for that exact TS2345. The ratchet only shrinks,
so type the mock instead of adding a baseline entry.
---------
Co-authored-by: Claude <noreply@anthropic.com>
The inline AI-note draft card in the diff view used a fourth, hand-rolled
box-shadow tier with raw rgba values instead of the documented shadow-xs
token, so it didn't track theme/token updates like the rest of the UI.
Restated the value under `.dark` too, since it shares selector specificity
with `.orca-diff-comment-popover`'s dark shadow later in the file and would
otherwise lose the cascade to that unrelated rule.
* fix(kimi): don't crash if config.toml is deleted mid-write
writeConfigToml checked existsSync(configPath) then called statSync(configPath)
to preserve the file's mode, with no error handling in between. If the file
was deleted in that window (e.g. a concurrent uninstall), statSync threw
ENOENT and the exception escaped install()/getStatus() uncaught.
Now a missing-file stat during that race is treated the same as a missing
file at the check: fall back to the default 0o600 mode and continue the
write. Any other stat error still throws, preserving the existing
mode-read-failure behavior.
* fix(kimi): use isDefinitiveAbsence for the config delete-race check
Reuse the repo's canonical absence check instead of a hand-rolled ENOENT
comparison, per review feedback on #23293. isDefinitiveAbsence also covers
ENOTDIR (an ancestor directory replaced by a file), which the inline check
missed but is equally "the config is definitively not there."
* perf: avoid rescanning partial notebook output frames
* perf(notebook): stream bridge frames and skip the unused size accounting
The reader buffered every record of a chunk before delivering the first one, and
asked the framer for byte accounting it can never use. An unbounded line limit
cannot reject, so the per-segment `Buffer.byteLength` and the rejection-prefix
retention were pure overhead for the notebook and Codex readers; both are now
skipped once, behind a hoisted check. Frames are handed to the consumer as each
line completes, so the first output of a chunk paints without waiting for the
last.
The dropped buffer only ever preserved a trailing partial record across a
consumer throw, which cannot help: that throw leaves the stdout 'data' listener
and takes main down with it. Rejections are no longer discarded either — the
bridge keeps fd 1 to itself, so an unreadable line means the frame channel is
damaged and now says so.
Tests move into notebook-kernel.test.ts beside the reader's existing coverage,
and add the never-terminated record and a guard that no record bytes are
measured when no limit applies.
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The WSL "is this path gone?" probe decided a path was missing by matching
GNU coreutils' exact wording, `stat: cannot statx ...`. BusyBox writes
`stat: can't stat ...`, so on an Alpine-style distro a successful orphaned
worktree delete was still reported as a permanent failure, on every retry.
Match only the trailing strerror text, which is POSIX and already pinned to
English by the probe's LC_ALL=C. Permission failures still report failure.
* perf(push): keep retention sweeps from overlapping
* perf(push): drain a saturated retention sweep instead of idling out the tick
The overlap guard on the shared prune timer removed a side effect the sweeper had
been relying on: overlap was the only thing that let a backlog exceed the
50-batch-per-call cap inside one 60-second tick. With the guard, a sweep that
spent its whole budget went idle for the rest of the interval, so a large backlog
drained far slower exactly when retention matters most.
The timer is now a chained setTimeout rather than an interval. `deleteInBatches`
reports whether it exhausted its batch budget, `prune()` returns
`{ deleted, saturated }`, and a saturated sweep is rescheduled immediately. The
connection gate hands a freed slot to the longest waiter, so one serial sweeper
looping back to back still parks a single statement ahead of a worker claim: claim
latency keeps the value the guard bought while the maximum drain rate returns to
what it was before. The loop is self-limiting and stops once the backlog clears.
A sweep that has not settled a full interval after it started now logs
`orca_push_prune_overdue` with its target. Admission waits have no timeout, so a
lost slot release could previously wedge retention permanently and silently.
Chaining makes the overlap guard structural, so there is no flag to scope. The two
single-DELETE sweeps state through `unbatchedSweep` that they have no batch budget
to exhaust, which keeps the immediate-resume path readable as delivery-only.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf: bound wildcard segment work in nested repository scans
* perf: bound the ** path walk in nested repository scans, not just one segment
The wildcard-segment fix left the larger blowup in place. A short .gitignore line
made only of `**` segments still costs exponential work in the outer path walk:
`**/**/.../z` at 24 segments and 73 characters, against an eight-segment
candidate, takes about 49 million recursive calls and ~150 ms here — larger than
the 25 ms single-segment case this branch removes. Rules are inherited down the
tree and re-checked for every directory, so that price is per directory, the
runtime's 15s scan timeout fires, and the user silently gets a short repo list.
Two independent bounds, both kept:
- `**` spans zero or more segments, so `**/**` accepts exactly what `**` accepts.
Parsing now collapses a run of them to one segment, taking the reported shape
from 49M recursive calls to 26 matcher steps.
- The walk memoizes on (pattern index, candidate index), so no other arrangement
of `**` can reintroduce the blowup. A rule holding at most one `**` is already
linear and skips the table, because allocating it costs more than the walk it
would save on the shapes real ignore files contain.
The budget suite's process-CPU ceiling is replaced by a matcher step counter read
through `readNestedRepoGlobMatchSteps`, so an algorithmic regression fails the
suite rather than passing on a fast machine. Each bound has its own budget case,
and each fails when only the other is applied.
The equivalence oracle now also covers multi-segment and anchored patterns
including `**`, checked against the pre-change per-segment expression walking
uncollapsed segments. Code-unit and metacharacter cases are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replace the copy-a-command startup dialog for diverged JSON/SQLite profile
state with Use SQLite / Use JSON buttons. The choice relaunches Orca into the
locked recovery preflight, applies it, then starts normally. Adds a
current-sqlite recovery selector (and --current-sqlite CLI flag) that archives
the diverged JSON and republishes it from SQLite.
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* perf(relay): release canceled AI Vault startup requests
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
* test(ai-vault): cover delayed readiness rejection across restart
* perf: stop scanning shared paths when suggestions are full
* fix(i18n): restore diff note draft catalog entries
* fix(windows): synchronize native terminal table lifetime
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix(ci): preserve focused Playwright file selection
* test(ci): require focused commands on every platform
Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes.
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
* test(windows): retain PTY stress failure evidence
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(ssh): suppress workspace echoes acknowledged during stale reads
* fix(ssh): preserve newer workspace observations during resync
* fix(ssh): retain newer own acknowledgements during stale reads
* fix(ssh): deliver peer snapshots cached by rejected patches during stale reads
A stale-revision patch reply caches the peer snapshot under a new host
observation token, but the renderer only records a conflict and blocks
uploads. Suppressing an identical stale read then left the peer change
unapplied. Only suppress when the intervening write kept the pre-read
token, which is what a contiguous own acknowledgement does.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(ci): preserve focused Playwright file selection
* test: isolate historical hourly build version inputs
* test: align historical package input with shared CI repair
* test: inject hourly package version without module mocking
* test: share the hourly package input contract across CI repairs
* test(ci): require focused commands on every platform
Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix: close abandoned PDF.js development asset streams
* test: keep casting safety comments attached after formatting
* fix(i18n): restore diff note draft catalog entries
* fix: guard closed PDF streams and preserve foreign listeners
* fix(i18n): make AI the recipient of diff notes
* test(browser): report the phase of WebRTC probe timeouts
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* test(persistence): start the fault timer after the real worker is ready
* test(orcad): preserve launcher startup phase evidence on timeout
* test: isolate historical hourly build version inputs
* test: align historical package input with shared CI repair
* test: inject hourly package version without module mocking
* test: share the hourly package input contract across CI repairs
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix: close abandoned static web readers and align AI note labels
* test: pin the historical hourly package version fixture
* test: provide the historical package version through build options
* fix: cancel abandoned relay searches
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix(windows): ship the process-table addon to the relocated daemon host
The Windows terminal daemon runs from a copy of the app under
%LOCALAPPDATA%\Orca\daemon-host\<version>. That copy took node-pty but not
@vscode/windows-process-tree, so the daemon's bare require of the addon found
nothing and every process-table read (foreground tracking, descendant sweeps)
fell back to a powershell.exe Get-CimInstance scan (#16905).
- Copy the addon's runtime files (package.json, lib/, the .node binary) into
the host; the ~25MB of gyp intermediates beside them are filtered out.
- Treat a host missing those files as unmaterialized, so hosts built before
this are rebuilt, and skip relocation if the install itself lacks them.
- Log the daemon's native/CIM capability at startup and warn once when the
process table falls back to CIM.
Revives #19525 on current main.
* test(windows): locate update-survival loss before relaunch
* test(windows): preserve daemon tree before update-survival proof
* test(windows): distinguish Electron exit from launcher close timeout
* test(windows): verify process exit when inherited pipes delay close
* test(windows): trace installer process checks in isolated survival runs
* fix(windows): probe process-query capability before installer sweep
* fix(windows): match installer probe and process-check profile behavior
* fix(windows): use NSIS separators for the process-check include
* test(windows): dismiss session-search overlay in survival harness
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Toolbar buttons fold into a dropdown menu when space is constrained,
preserving minimum address bar width. ArtifactPublishButton gains
controlled popover state with optional virtual anchoring. Tour-pinned
tools stay visible while others fold in priority order.
* fix: release dictation session listeners when speech workers exit
* fix(i18n): restore diff note draft catalog entries
* fix(i18n): make AI the recipient of diff notes
* test(persistence): start the fault timer after the real worker is ready
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* docs: clarify native chat document ownership
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix: fence callbacks from retired emulator streams
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* perf: scope relay capacity checks to the requested cell
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix: observe abandoned linked-issue lookups on desktop and runtime
* fix: restore catalog entries required by the current CI baseline
* fix(i18n): make AI the recipient of diff notes
* fix(i18n): restore diff note draft catalog entries
* fix(i18n): make AI the recipient of diff notes
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>