* fix(ci): stop hourly versions dropping below a tagged or already-shipped build
Hourly/daily/adhoc based their X.Y.Z on GitHub releases, not git tags. When
v1.4.202 was tagged and then its GitHub release vanished, the next hourlies
shipped as 1.4.202-hourly — below both stable 1.4.202 and the 1.4.203-hourly
builds already installed, so electron-updater stopped offering updates.
Read main's v* tags and already-published channel tags instead.
* docs(ci): record that 1.4.202's release was unpublished for a bug
The leftover tag is what hourly must still honor; this was not a failed cut.
* feat(ai-vault-search): persist agent-session search consent and retention
Two booleans and nothing else: `enabled` and `historyDays`, off by default
because building the index reads every transcript on the machine. No `paused` --
the PR 3 indexer is immutable, so every change is close-and-construct.
The settings IPC normalizes a write like every other field and hands the change
to the index; there is no UI for it until PR 8.
* feat(ai-vault-search): hold one indexer and engine pair per host
The object that owns a host's live index and the three recipes that change it.
The indexer is immutable, so a settings change is close-and-construct, disabling
is close with no replacement, and clearing is close, remove the database,
construct. The new instance's first sweep purges a narrowed window and admits a
widened one, so neither needs a code path.
The database sits beside the scanner's parse cache, one file per host. A runtime
with no node:sqlite can hold no index at all, which the Node 18 floor on orcad
and the relay makes a real case rather than a hypothetical one.
* feat(ai-vault): let the scanner child own the session search index
The transcript reader runs in that child, so the index consumer has to as well:
one read serves both the session list and the index. Three request operations
(search, status, reconcile) and one fire-and-forget settings message carry
everything a parent needs; main never opens the database file.
The init frame becomes a factory because it is read at every spawn, so a
respawned child sees current consent rather than the first frame's. A child
holding a running index is never idle from the parent's side, so idle retirement
is suppressed while the index is on -- retiring it would stop the reconcile loop
until some later scan happened to respawn one.
Both files this lands in were already at the max-lines ceiling, so three
collaborators move to where they belong rather than being disabled around: the
invalidation deadline into the class that owns invalidations, call cancellation
and the start requeue into the call-state module, and orcad's flag parsing into
its own file.
* feat(ai-vault-search): register a search service on every host that answers
Without a registered service a host answers no-service, which means "this host
does not have the feature" rather than "the index is off". All three hosts now
answer the second thing.
The desktop forwards to the scanner child. orcad and the SSH relay daemon have
no such child -- orcad ships only the watcher and daemon entries, and the relay's
AI Vault sidecar runs the remote scanner, which publishes nothing to the
transcript channel -- so on those two the index lives in the process that would
drive its reads, gated on a runtime that has node:sqlite at all.
The relay registers with consent off and no way to turn it on: nothing carries a
setting to a remote host yet. That is the honest state, and it is still worth
registering, because it is what tells a client the difference between off and
too old.
* test(ai-vault-search): price a warm pass over five thousand transcripts
The number the reconcile interval will be revisited against, measured rather
than argued: a warm sweep stats every file under every root, a warm cycle stats
the newest N per agent, and neither reads what the index already holds. It does
not tune the interval.
* fix(ai-vault-search): answer the casting gate without assertions
main's new type-assertion rule reaches every file this branch touches. All nine
sites drop the cast rather than carry a SAFETY: rationale: the operation guard
narrows with `in`, the sqlite probe narrows the builtin it loads, the child test
keeps the discriminated reply instead of widening it, and the settings resolver
takes `unknown` -- which is what it really reads, since a persisted profile can
hold a value no version of this code wrote.
* fix(ai-vault-search): let a refreshed scan root reach the live index
The parent re-resolves scan roots before every policy push, precisely so a
WSL distro or extra Codex home that appeared since the child spawned enters
the window. The child forwarded only the settings to a live instance and used
the roots solely in its `??=` initializer, so those roots were dropped for the
child's lifetime.
The indexer stays immutable: a structurally different root set closes the pair
and constructs a new one, the same way a changed databasePath already does.
Compare via `sameSessionSearchRoots` rather than a plain JSON compare, because
nothing fixes the key order two producers write; lists are sorted too, since
the indexer walks every root and a re-enumeration that reorders is not a
change. An unchanged set still never restarts a running index.
The orcad and relay in-process hosts resolve roots once at install and never
re-apply, so they have no such seam.
* fix(ai-vault): restart the scanner child the index is holding
Three review items.
The hold keeps a child alive for the index, but only a queued call ever
started one: `pump()` skipped a hold with an empty queue, so an idle indexing
child that crashed, or an `ensureChild()` that failed at start, left indexing
stopped until an unrelated request happened to arrive. `pump()` now starts the
child the hold requires, which is also the restart callback the fault policy
already schedules, so the existing delay and circuit bound the retry exactly as
they bound a queued call's start. `updateSessionSearch` goes through the same
seam instead of its own `ensureChild` call.
A search registers no AbortController, so a cancel sent for a search id was
added to the `cancelled` set and never consumed. Nothing can reach that today
-- no caller passes a signal and the child answers in milliseconds -- so this
is only a leak of ids: consume it when the search settles.
The orcad argument doc claimed a `--`-prefixed value stays a flag. The parser
takes the next token regardless, and orcad-launch-contract.test.ts pins that,
so the doc is what was wrong. Behaviour is unchanged.
* fix(ai-vault): recover search indexing and refresh scan roots
* fix(ai-vault): defer search refresh policy reads
* fix(session-search): stabilize paging and host enablement
* fix(session-search): refresh host roots within full sweeps
* docs(session-search): clarify initial root fallback
* perf(persistence): add pty-binding fast lane to skip redundant flushes
Terminal pane reattachment currently clones the session and serializes the
entire 9.2 MB app state even when the binding is already in place and durable.
Add an early-return fast path that skips this work when all nine predicates
hold: no split, binding matches in-memory and on-disk, incarnation matches,
no tombstone, and generation counter proves durability.
Includes one-line fix in `writeToDiskSync` to record hash-matched sync flushes
as durable, so the fast path doesn't stay parked behind a stale generation.
Adds `persistence.pty-binding` observability spans (local NDJSON, unsampled for
mutations, budgeted for fast-lane hits) to measure eligibility rates before
and after. Includes ratchet test to ensure every binding writer bumps the
generation. Diagnostic tools and full investigation notes from September 7,
2026 capture that identified the 59–100 ms no-op binds and measured a real
terminal keystroke queued 117 ms behind one such call.
* perf(persistence): add pty-binding fast lane to skip redundant flushes
Rapid rebinds of already-durable PTY bindings (e.g., remounting panes)
were unnecessarily expensive because they cloned and flushed the entire
document state every time. Detect when a binding hasn't changed since the
last durable write and skip to return immediately, eliminating main-thread
cost on that path.
* perf(persistence): record binding.origin on the pty-binding span
Fresh spawns always flush, so a fast-lane rate over all calls is diluted
by however many terminals the user opened. Each caller knows whether it
is a spawn, a reattach, a split, or a relay reattach; pass that through
as metadata and record it so the reattach hit rate can be read from the
trace file. Never branched on.
* fix(persistence): keep the tab row on its first pane when a sibling pane binds
A tab row names one PTY, but a split tab holds several panes. The
renderer keeps the row on the first pane and refuses to let later
split-pane spawns steal it, since a remount reattaches the tab to
whatever the row says. Main overwrote it with whichever pane was binding,
and the renderer's next publish put it back, so every sibling reattach
was a state change and could never take the fast lane. On the real
profile that is 38% of panes.
Rewrite the row only when it names nothing useful: null, the PTY this
leaf is replacing, or a PTY no leaf holds. The fast-lane predicate
compares against the same rule.
* perf(persistence): record durable pty-binding flushes per pane
The global write generation is held back by any unrelated dirty
state, causing bindings unchanged for minutes to appear unpersisted
despite being on disk. Track per-pane durability to skip redundant
flushes.
* docs(persistence): describe the per-pane durability record
The durability section still described the global generation check as the
whole story and claimed there was no binding durability cache. Record the
measurement that motivated the per-pane record, and why retiring one needs
no cooperation from other binding writers.
* docs(perf): consolidate every measured Orca performance issue into one register
Folds the findings from all related debug sessions into the live lag
investigation: the persistence/main-thread work (P1-P11), host contention
(H1-H5), git and subprocess load on main (G1-G8), renderer and terminal
rendering (R1-R8), the terminal daemon session leak from the deleted
debug-orca-perf-issue worktree (D1-D9), and the Cmd-J palette review (C1-C6).
Keeps the measurement behind each claim, records what is fixed versus open,
and restates what the 117 ms keystroke delay still does not explain.
* fix: address performance review findings
* fix: satisfy diagnostic probe lint
* chore: keep investigation artifacts out of performance PR
* fix: run lag probe regression tests with Vitest
* perf(persistence): replace pane receipts with global durability check
* refactor(persistence): remove redundant binding review machinery
* test(persistence): satisfy current assertion-free quality gate
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* Widen Windows shim ratchet to detect package bin spawns
Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(scripts): fold dot segments before matching node_modules/.bin
The predicate joins call arguments textually, so a literal '..' segment hid a
path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and
Windows separators) first. A '..' that genuinely escapes .bin still does not
match.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* style(scripts): use .at(-1) in the dot-segment fold
oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* Add compile-time RPC params catalog parity gate
Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(rpc): keep the params generator off its own output
The parity gate imports the generated catalog for types, and it lives under
RPC_DIR, which indexableModules() scans for shared imports. That re-added
OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d
the committed catalog. A catalog referencing a renamed or deleted shared export
then crashed regeneration — in exactly the state that requires regenerating.
Reproduced before and after: with a dangling reference injected into the
catalog, `generate:rpc-params-catalog` threw; it now rewrites the file.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* perf(tooling): reuse directory entry types in source scans
* fix(source-scan): stat DT_UNKNOWN dirents so untyped directories are still walked
`readdirSync(..., { withFileTypes: true })` can hand back a Dirent whose
type the filesystem did not report. For that entry every predicate is
false, so the readdir-type fast path treated a real directory as a file
and silently dropped its subtree from every ratchet guard. Fall back to
`statSync` whenever the entry is neither conclusively a file nor a
directory, keeping the no-stat fast path for ordinary entries.
Also make the two readdir-order assertions in the walk test
order-independent; `scanSourceTree` returns raw readdir order, which
differs on tmpfs.
* test(source-scan): unit-test the stat fallback via an extracted helper
The fabricated-Dirent readdir mock could not satisfy both gates at once:
vi.mocked(readdirSync) resolves to Node's Dirent<NonSharedBuffer> overload, so
the mock needed a type assertion, and #19462's casting gate rejects new ones on
changed lines. Removing the cast then failed tsc.
Extract directoryEntryNeedsStat and test it directly with a structural probe.
No mock, no cast, no top-level await, and the DT_UNKNOWN case is pinned:
removing the fallback fails 'stats an entry whose type readdir could not report'.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* Reduce native dependency installs to the host platform
* Remove install policy documentation
* Guard cross-arch packaging and scope release installs to the runner
electron-builder only logs a warning for a missing extraResources source,
so a host-only install silently shipped a foreign-arch slice without its
natives — `pnpm build:mac` on Apple Silicon produced an x64 DMG with no
sherpa-onnx-darwin-x64 and no @parcel/watcher-darwin-x64. The previous
beforePack hook covered only win32.
- Add assertPackagedNativeVariantsInstalled, an arch-aware check over the
target's sherpa-onnx, @parcel/watcher, and (on Windows) node-gyp addons.
beforePack now runs it for every platform, with remedies split: another
architecture comes from install:release, the os:win32 addons need a
Windows host.
- Drop --os from the release installs. Every packaging job already runs on
a runner whose OS matches its target, so only the macOS lanes need extra
breadth, and only on CPU for their x64+arm64 config. Windows and Linux
packaging return to a plain host-only install.
- Add --frozen-lockfile to install:release so a bare run cannot rewrite
the lockfile.
- Restore the install policy reference doc and the CONTRIBUTING note, plus
the rationale comments dropped from the runtime contract test.
- Gate the packaging-closure assertions on whether the Windows addons are
installed rather than on the host OS, so a cross-arch install exercises
them off Windows too.
- Make the workflow contract test read `run:` steps as well as retry-action
commands, and enforce host-only scoping on the non-macOS packaging lanes.
- Remove the unreferenced install measurement script; its numbers live in
the policy doc.
* Track the install policy doc and index it from AGENTS.md
docs/** is ignored behind a per-file allow-list, so the new reference doc
was only committed via git add -f and future edits would be skipped. Add
it to the allow-list and give it an AGENTS.md entry like every other
tracked reference doc, so the host-only install rule is discoverable
before someone packages a second architecture.
* Route Windows-lane removals through the retrying helper
Adding these four specs to the PR Windows lane pulled them into the
windows-lane-tree-removal-boundary ratchet, which failed on 20 raw
recursive removals. On Windows a bare rmSync races a handle the OS has
not released, throwing EPERM after the assertions already passed and
reporting a green test as a lane failure.
* Adapt the packaging guard to the vendored Windows registry addon
main vendored windows-native-registry as the workspace package
@orca/windows-registry (#20438). A workspace link resolves on every
host, so including it in the installed-Windows-addons checks proved
nothing. @vscode/windows-process-tree is the only os: win32 npm addon
left, so it alone decides whether the win32 resource plan resolves.
* build(macos): run native module builds concurrently
* fix(build): terminate sibling native builds when one fails
Address coderabbit review: concurrent builds kept writing native
artifacts after a sibling reported failure. Track spawned children,
kill remaining siblings on first nonzero exit, and forward SIGINT/
SIGTERM to all children.
* fix(build): process-group teardown and prefixed output for parallel native builds
Address second coderabbit round:
- Detached process groups + negative-pid kill so SIGTERM reaches swift/
swiftc descendants, not just the direct pnpm child (they could keep
writing artifacts after fail-fast)
- Signal handlers preserve the received signal (SIGINT no longer becomes
SIGTERM for children) and are removed before re-raising, so the parent
actually dies instead of looping through terminateAll
- runPnpmScript settles only on close, never on error alone, so
Promise.all cannot exit while children are still running
- Per-module output prefixes ([computer]/[keyboard-layout]/[notification-
status]) match what the PR description always claimed; interleaved
swiftc errors are now attributable
- Windows path untouched (early return before any of this runs)
execa/p-limit were considered and rejected: no new runtime deps for a
build script, and detached process groups give strictly stronger cleanup
than execa's direct-child kill.
* fix(build): memoized handler removal and external-vs-sibling signal split
Second-round coderabbit findings on 24392a0:
- Registration now uses the memoized handlerFor() instances so
removeListener actually removes them (inline arrows were never
registered, so the parent looped through terminateAll and hung)
- externalSignal is set only by the parent's own signal handlers; a
sibling's fail-fast SIGTERM no longer masquerades as an external
signal, so settle() resolves Promise.all with the failing module's
exit code instead of leaving top-level await unsettled (exit 13)
- Also fixes a TDZ crash: handlerFor() was invoked at registration time
before the signalHandlers const initialized
Verified: sibling fail-fast resolves failer=7 with no survivors;
external SIGINT kills children then the parent exits 130; real
concurrent macOS build green.
* Wait for native build cancellation before exiting
* Clean up native builds when output streams fail
* fix: bound native build waits, forward SIGHUP, honour output backpressure
- Bound the per-child close wait: two seconds after a child exits, reap
its process group and destroy its pipes so a descendant that inherited
stdout/stderr cannot hang `pnpm build:native` forever.
- Handle SIGHUP alongside SIGINT/SIGTERM so a terminal hangup reaches the
detached compiler sessions instead of orphaning them.
- Pause a compiler's output stream when the launcher's stdout/stderr
reports backpressure and resume on drain, so prefixed output no longer
buffers without bound.
- Run build-native-for-platform.test.mjs in the computer-e2e
mac-native-owner-smoke PR job and trigger that workflow on launcher
changes; the tests are darwin-only and no other PR job runs on macOS.
- Report the first failing child's status: re-raise its signal, or use
its exit code instead of Math.max over cancelled siblings.
* fix(native-build): keep output when reap timer overlaps backpressure; fail on ignored re-raised signal
The descendant reap timer started on every child 'exit' and fired even when
'close' was late only because the launcher paused the pipe for its own stdout
backpressure, destroying pipes with compiler output still queued. Arm the
countdown only while the pipes are actually draining: clear it on 'pause' and
re-arm on 'resume' after exit. Write the reap notice to stderr since stdout
is the stream that may be blocked.
Re-raising a child's fatal signal is a no-op when Node ignores it (SIGPIPE),
so set a non-zero exit code first; a failed build no longer exits 0.
Tests: stall the launcher's stdout consumer past the reap timeout and assert
every kernel-accepted compiler line still arrives; kill the computer build
with SIGPIPE and assert the launcher exits 1.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
On Windows, pnpm shortens the virtual store directory to
@vscode+windows-process-tre_<hash>, cutting into the package name before
the @, so the @vscode+windows-process-tree@* glob matched nothing and the
addon recompiled on every Windows job. node-pty escapes this because its
truncation lands after node-pty@, which the glob still matches.
Widening the prefix to @vscode+windows-process-tre* matches both the full
name kept on macOS/Linux and the truncated Windows one.
The workspace link means pnpm never creates a .pnpm/@orca+windows-registry@*
entry, so all four native-cache blocks globbed a path that cannot exist and
the addon was recompiled on every Windows job.
Also hardens the addon itself: RegEnumValueW reports a byte count and the
registry does not enforce whole WCHARs for string types, so an odd count let
Napi's auto-length scan run past the value; and a value named __proto__ would
reassign the result object's prototype instead of becoming an entry.
* Add casting code quality lint scan
Enforce type assertion style by adding a new oxlint scan with `typescript/consistent-type-assertions` rule. Requires using `as const`, type annotations, or `satisfies` instead of raw type casts, with documented `SAFETY:` exceptions for unavoidable cases.
* fix minor issue
* refactor(windows): vendor the registry addon as @orca/windows-registry
windows-native-registry@3.2.2 was last published in 2023 by a single
maintainer. Orca called two of its exports, both read-only, so the whole
dependency is replaced by a local N-API addon under native/.
The vendored addon is read-only by construction: setValue, createKey and
deleteKey are gone, so RegDeleteTreeW no longer ships in the app. Two
upstream defects are also fixed rather than carried over — the name/data
scratch buffers were file-scope statics that concurrent reads would
scribble over, and createKey/deleteKey called .c_str() on a temporary.
Build wiring keeps the existing shape: still an optionalDependency gated
to win32, still excluded from pnpm's allowBuilds so only Orca's own
Windows rebuild runs node-gyp for it, still copied into the packaged
resources. The CI native caches now key on the vendored sources so an
addon.cc edit cannot restore a stale .node.
* test(windows): check the vendored registry addon against reg.exe
The addon is vendored source, so no upstream release proves it still
decodes values the way Orca's PATH readers expect. reg.exe is the only
independent oracle on the box.
* ci(windows): register the registry addon test on the Windows runner
A Windows-gated file self-skips on ubuntu, so without both registrations
it reports success while running on no machine at all.
* fix(build): link the registry addon as a workspace package, not file:
As a `file:` dependency pnpm re-resolved and re-linked the package on
every install, including `--frozen-lockfile` (measured: "added 1" on a
repeat no-op install). That virtual-store churn ran concurrently with
node-gyp reading the same tree and cost @vscode/windows-process-tree its
binding.gyp mid-rebuild, failing package (windows) whenever the native
cache hit and only that module needed building. The linux packaging job
hit the same race from the other side, as a pnpm staging move failure.
A workspace link resolves once and leaves the store alone; repeat
installs are now 55ms no-ops. native/windows-registry is listed
explicitly so `packages:` still does not auto-discover mobile/.
* fix(build): stop tracking node-gyp output for the vendored addon
The build/ tree is generated per host and ABI; the committed copy was
macOS-specific gyp scaffolding from a local build and would have shipped
stale Makefiles to every checkout.
* chore: ignore the vendored addon's node-gyp bin output too
node-gyp also emits bin/<platform>-<abi>/ beside build/; both are per-host
generated output that must never be committed.
* chore: remove duplicate and unused documentation media
* chore: guard README local links and refresh tile-01 vendor metadata
- Add config/scripts/check-readme-local-links.mjs: every local src/srcset/href
in README.md and docs/readme/*.md must resolve to a tracked file. Runs in the
ungated root_directory_guard job so docs-only diffs (which skip static_analysis)
still catch a deleted docs-site or feature-wall asset the README embeds.
- Refresh tile-01.recorded-at.json to what vendor-feature-wall-assets.mjs now
emits for the tab-split source path.
- Drop the pr-19217 evidence prose that cited the removed screenshots.
* fix: accept single-quoted attributes in README local link check
The parser only matched double-quoted src/srcset/href, so <img src='missing.gif'>
was skipped and the guard passed a README that GitHub renders with a broken image.
Regression test fails without the parser change.
* perf(android): queue fragmented scrcpy video packets
* fix(android): release consumed scrcpy chunk storage
* fix(android): bound queued scrcpy fragment count
- Coalesce pending video fragments once more than MAX_PENDING_CHUNKS
(1024) are queued, so a large frame delivered in tiny socket chunks
cannot retain millions of Buffer objects below the 16 MiB byte guard.
- Add a regression test feeding a 256 KiB frame one byte at a time and
asserting the retained fragment count stays bounded.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(chat): stream journal replay without retaining obsolete revisions
* fix: page journal replay reads so no SQLite snapshot outlives its statement
- iterateJournalEpochRows fetches one completed LIMIT statement per page
instead of a lazily consumed .iterate() cursor, so reduction never runs
inside an open read snapshot and a WAL checkpoint can pass mid-replay.
Regression test: a checkpoint issued from inside the reducer is not busy.
- The retention test now asserts the applyJournalRow spy observed every
row, so the 8 MiB bound cannot pass vacuously if the spy stops
intercepting.
- Reliability gate manifest records the new assertion and the paged
read design.