mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
12ef12c55bbf73af32f1bd69e5ae2f0bea8adfa8
279
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
12ef12c55b |
chore(quality): ratchet Oxlint, React Doctor, and Zustand performance (#11034)
* chore(quality): ratchet lint and Zustand performance * fix(ci): stabilize React peer lock snapshot * fix(ci): isolate PR diff and React Doctor CLI |
||
|
|
c6076a507c |
ci(release): detach non-blocking E2E (#11031)
* ci(release): detach non-blocking E2E * test(release): pin E2E dispatch retries |
||
|
|
3d98cda5b2 | fix(release): accept lowered telemetry declarations (#11019) | ||
|
|
0edc95fa35 |
perf(editor): cut per-keystroke work on two rich-markdown paths (#10862)
* perf(editor): cut per-keystroke work on two rich-markdown paths Doc links: both plugins walked every text node and ran matchAll on each — the auto-convert appendTransaction once per keystroke, the preview decorations once per keystroke and again per caret move. A link needs `[[`, so gate on a native substring check first. The two walks had duplicated their guard sequence; they now share one predicate. 3.1x-3.8x over the repo's own markdown. Annotations: resolving a comment's block re-serializes the whole document (every node, plus every adjacent pair), and both the highlight-range and comment-at-position paths did that once per comment — O(comments x document). Build the blocks once and pass them down. On a 12-node fixture with 8 comments that is 184 serializations down to 23. * test(editor): pin the one-build serialization baseline Review feedback, all four points: - The serialize-count assertions compared many-comments against one-comment, so they would have passed if BOTH built blocks twice. Pin the absolute count (23 = 12 nodes + 11 adjacent pairs) derived from the fixture size, so a regression to per-comment building fails instead of comparing equal. Verified by reverting the hoist: 2 tests fail. - Skip an empty benchmark corpus instead of evaluating `index % 0` and dereferencing undefined. - Build fixture paths with path.join. - Condense the benchmark header to purpose plus parity guarantee. Co-authored-by: Orca <help@stably.ai> * test(editor): harden doc-link performance evidence Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a49d68f8c2 |
perf(git): overlap getBranchCompare's head-of-chain reads (#10895)
* perf(git): overlap getBranchCompare's head-of-chain reads
Four git spawns ran strictly in series before any compare work began:
branch --show-current, the base-ref probe, rev-parse HEAD, and rev-parse <base>.
Three are independent -- compareRef is display-only metadata and HEAD's oid does
not depend on the base ref -- so they now run concurrently. The fourth was
redundant outright: the probe already runs `rev-parse --verify --quiet
<ref>^{commit}` and discarded the oid it printed, which was then re-resolved by a
second spawn. resolveWorktreeBaseCommitOid returns that oid so it can be reused;
hasWorktreeBaseCommitRef now delegates to it, leaving its other 4 callers
untouched.
3.6-3.7x on a short remote base label (192ms -> 52ms), 1.44x on an
already-qualified refs/... base, which skips the probe by design.
Reuse is keyed by ref: resolveWorktreeAddBaseRef returns at its first successful
candidate, so only that ref's oid is ever read back. Peeling is safe because only
refs/heads and refs/remotes candidates reach the probe, where ^{commit} is a
no-op.
No new git features: this removes a spawn rather than adopting an option.
Co-authored-by: Orca <help@stably.ai>
* fix(git): preserve compare semantics across providers
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
6677b5f171 |
perf(cli): construct the runtime client only when a command needs it (#10919)
src/cli/index.ts was the only eager value-import of RuntimeClient, and five other eager modules imported just RuntimeClientError / RuntimeRpcFailureError from the runtime-client barrel -- dragging in client -> pairing -> zod -> ws -> e2ee on every invocation. Those error classes live in runtime/types.ts, which has zero children, so the five imports now point there and the client loads through the existing (already lazy by design) ctx.client getter. Eager modules 199 -> 46, with node_modules dropping 94 -> 0. `orca --help` 2.04x (59.6 -> 29.2 ms); the same for help, no-args, and both error paths, which return before constructing a client. Commands that DO construct one still gain 1.10-1.12x from not eagerly parsing the transport the local path never uses. Correction to an earlier note: websocket-transport alone is ~24 modules / ~8 ms, not the 107 / 28 ms once recorded -- that figure wrongly charged it for zod, which enters through shared/pairing on a different edge. Marginal cost, never isolated cost. Co-authored-by: Orca <help@stably.ai> |
||
|
|
077561f89a |
perf(terminal): measure stream byte length natively above a code-unit floor (#10916)
* perf(terminal): measure stream byte length natively above a code-unit floor The terminal RPC path counted UTF-8 bytes with a hand-rolled per-code-point scan that Buffer.byteLength does natively an order of magnitude faster. Routed through a small module rather than swapping the shared clipboard helper, which has ~50 renderer call sites and a partial-count contract on the over-limit path that must not change. 4.4x on an 8KiB batcher push, 4.2x on a 2MiB snapshot scan, 4.0x on the 48KiB chunk gate, and 1.26-1.33x on the adversarial early-trip shapes where the legacy scan bails after a third of the string. The floor is load-bearing, not defensive. Buffer.byteLength has a fixed ~14ns call cost against a scan iteration of ~1.5ns, so below the measured 8-12 code unit crossover the native call is a REGRESSION -- 4.2x slower at one code unit, which is keystroke echo, the most latency-sensitive PTY shape there is. Short inputs keep the scan verbatim; 16 leaves margin over the crossover so the worst sub-floor shape stays at parity. measureTerminalStreamByteLength takes the native count only when `length * 3 <= stopAfterBytes` proves the limit cannot trip, so the callers' truncated running total is never replaced by a full count. Co-authored-by: Orca <help@stably.ai> * test(terminal): benchmark production byte-length exports Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
6943638053 |
perf(terminal): scan output frames by code unit, not per code point (#10915)
* perf(terminal): scan output frames by code unit, not per code point iterateTerminalOutputFrameChunks walked `for (const part of data)`, materializing a 1-2 character string per code point and calling terminalStreamByteLength on each, while accumulating the frame text with `chunk += part`. The accumulator was never needed: `chunk` only ever reconstructs the contiguous substring data[chunkStart..end), and `startSeq + chunkStartOffset + chunk.length` collapses algebraically to `startSeq + end`. Track two integer offsets and emit data.slice(chunkStart, end) instead, computing UTF-8 width inline from charCodeAt. Also short-circuits the cap gate on UTF-16 length before measuring UTF-8 bytes, which is sound because UTF-8 length is never below UTF-16 length. 2.4-6.1x across payload shapes, stable across reruns. This runs per terminal output batch and per snapshot chunk. Extracted to its own module along a real seam (the chunk-emission concern plus its two types and cap gate); methods/terminal.ts shrinks by 85 lines. No max-lines suppression added and the baseline is untouched. Co-authored-by: Orca <help@stably.ai> * fix(terminal): preserve chunk sequence rounding Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f48cb78646 |
perf(store): index worktrees and tabs once per hydration loop (#10891)
* perf(store): index worktrees and tabs once per hydration loop Four sites in hydrateWorkspaceSession and reconnectPersistedTerminals rebuilt a flattened array inside a loop and linearly searched it: Object.values(worktreesByRepo).flat().find((e) => e.id === worktreeId) That is O(rows x ids) for O(rows + ids) distinct work. Build one first-wins index per loop instead. Neither loop sets state or awaits, so a single index over the store snapshot is valid for every iteration. 54.5x at a real 10-repo / 423-worktree session with 188 pending reconnects; 2.1x on a one-repo session. This runs synchronously on renderer cold start and gates workspaceSessionReady, which blocks terminal pane mounting. First-wins matters: Array.prototype.find returns the first match, so an index that overwrote on collision would resolve a different repo for a duplicated worktree id. Both the tests and the benchmark fixture carry a deliberate cross-repo duplicate so that difference is observable. Co-authored-by: Orca <help@stably.ai> * test(store): count the generated worktree rows instead of multiplying Review feedback: the table printed repoCount * worktreesPerRepo, which misses the duplicate id makeStore injects for multi-repo cases (that duplicate is what makes first-wins observable). Count the generated map, and say plainly that the fixtures are synthetic at real-world scale rather than a replay of a real session. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
79ec57d045 |
perf(relay): keep the PTY replay window as chunks, not a re-sliced string (#10900)
* perf(relay): keep the PTY replay window as chunks, not a re-sliced string appendReplayBuffer did `buffered += data` then `buffered.slice(-REPLAY_BUFFER_MAX)` once over the 100KB cap. It runs per raw node-pty emission -- before batching -- so once a PTY saturates the window (which a long-lived shell does almost immediately) every subsequent chunk copied the whole 100KB. Reuse RecentPtyOutputBuffer, which already solved this shape in the main process: keep chunks, drop from the head, defer the join to read(). The relay's three readers are attach, adopt, and revive only. 66-205x on the append path, per PTY, on the user's SSH host. RecentPtyOutputBuffer's limit is now configurable, because the relay retains 100KB where the main process retains 64KB. One arithmetic branch still used the hardcoded constant after that change and silently under-retained (100,800 of 102,400 code units); the equivalence tests caught it before it shipped, and the suite now pins the configured limit directly. Co-authored-by: Orca <help@stably.ai> * test(relay): exercise a real surrogate split; drop eval from the benchmark Review feedback, both valid: - The surrogate test never split a pair. The cap is even and a pair is two code units, so an emoji run alone always cuts on a pair boundary. A trailing single unit shifts the cut mid-pair, leaving a dangling low surrogate (0xDE00) -- asserted directly now, with the boundary-aligned case kept as its own test. - Parse REPLAY_BUFFER_MAX as a product instead of eval(). The regex already admits only digits, spaces and `*`, and eval tripped Biome's noGlobalEval regardless of the eslint suppression. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
10ca89ac8b |
feat(updater): switch to validated local mac builds (#10889)
* feat(updater): switch to validated local mac builds * test(updater): cover local build recovery actions * fix(types): keep local build contract in project sources |
||
|
|
abcdc04f6b |
fix(ci): mirror missing lint steps in PR workflow (#10601) (#10623)
Reviewed with an independent reproduction. Added the allowlist entry that unblocked verify:localization-coverage on main, the 4th drifted step, and a parity gate that fails when pnpm lint's chain contains a script absent from pr.yml. |
||
|
|
39a200d900 |
fix(release): restore the Windows inner-binary signature gate (#6487) (#10719)
* fix(release): restore the Windows inner-binary signature gate
electron-builder 26.9+ dropped the bundled 7zip-bin package, so the gate's
hardcoded node_modules/7zip-bin path stopped resolving in
|
||
|
|
0f91af821d |
ci: parallelize PR checks and accelerate Vite builds (#10989)
* ci: parallelize and accelerate PR checks * fix(ci): make accelerated checks runtime-safe * fix(ci): address review findings * fix(ci): retry transient Electron downloads * test(ci): cover Electron download retry limits |
||
|
|
cd05f2ff93 | Implement robust orchestration primitives and connected-server workers (#9925) | ||
|
|
fb26254a02 |
perf(usage): yield with setImmediate, not a clamped setTimeout(0) (#10892)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
165e4e0d1b |
perf(agent-status): strip terminal control bytes by run, not per character (#10866)
* perf(agent-status): strip terminal control bytes by run, not per character stripTerminalControl built its result with a per-character `+=`, allocating a fresh string for every retained character. The Command Code status detector calls it four times per PTY chunk — the scan text, the chunk-boundary variant, and both previous-text lengths — so an agent pane paid that on every write. Control bytes are sparse in real output, so copy the spans between them instead: 2.3x-2.6x from 5 KiB to 106 KiB chunks. Output is byte-identical, checked exhaustively over every string up to length 4 across a 13-symbol control/unicode alphabet plus 200k random strings (224,831 inputs, 0 mismatches). * docs(agent-status): condense the run-copy rationale comments Review feedback: both comments walked through the implementation. Keep one line of non-obvious rationale each, per the repo's comment guidelines. Co-authored-by: Orca <help@stably.ai> * test(agent-status): correct terminal strip benchmark * test(agent-status): bound terminal strip benchmark --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
97e4776dfe |
feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental) (#8549)
* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental) Adds Orca's experimental plugin system behind a settings flag: a supervised kernel, declarative content packs (VM recipes, commands and keybindings, language packs), sandboxed iframe panels, forked worker hosts, and a Git-backed marketplace v0 with consent, provenance and kill-list enforcement. Theme, icon-theme and terminal-theme contributions are deferred to a follow-up pass. * fix(plugins): make unsupported marketplace listings unreachable by key findPlugin() backs preview/install/previewInstalledUpdate via requireListing(), so filtering only listPlugins() hid the catalog card while leaving the dead install path reachable one click later. * fix(plugins): fan Pi session-only status out to plugin subscribers The providerSessionOnly early-return in applyNormalizedStatus emitted to onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so plugins subscribed to agent.status.changed silently missed every Pi session_start event. Route both emit sites through one helper so a future early return cannot drop the plugin tap again. Co-authored-by: Orca <help@stably.ai> * plugins: drop dead code and hoist duplicated trust-boundary patterns Cleanup pass over the P1 diff, no behavior change: - Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus the now-vestigial `directories`/`signal` plumbing in `collectFiles`. - Delete `resolveContainedPluginDirectory` (no callers). - Delete `plugin-content-load-pool.ts`; it reimplemented the existing `mapWithConcurrency`, whose index arg also removes the pairing wrapper in `buildPluginList`. - Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into the install-lockfile module; 11 sites hand-rolled these identically. - Point the new reliability gate at the PR instead of gitignored docs paths, matching every other gate's link form. * fix(plugins): retry plugin state renames on Windows AV/EPERM locks Six plugin write paths (lockfile, provenance, current pointer, kill list, marketplace cache, staged install dir) did a plain rename, so an antivirus or indexer holding the target open surfaced as a failed install. The repo already retries this hazard for issue #1507, but only through a sync helper; these paths are all async. Adds one bounded async retry + atomic write used by all six, and trims a consent-provenance header that restated its own JSX. * test(plugins): cover the Windows rename retry path The retry loop shipped untested: both existing cases hit the non-retry path, and the temp-cleanup test passed identically with the `finally` removed. Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke. Co-authored-by: Orca <help@stably.ai> * fix(plugins): pin bundled plugin resources to LF Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived as CRLF and verify-packaged-plugin-resources rejected it — the packaged build could never pass on Windows. Reproduced locally: CRLF yields the exact CI error, LF verifies clean. Files are already LF, so nothing renormalizes. Co-authored-by: Orca <help@stably.ai> * test: guard the bundled-plugin LF pin against a CRLF checkout The byte-hash mismatch only surfaced in Windows packaging CI. Assert the .gitattributes pin and that a CRLF tree is rejected, so a regression fails on any platform instead of waiting for a packaged Windows build. Co-authored-by: Orca <help@stably.ai> * ci: trigger packaged-build check on bundled plugin resource changes The launch tree is byte-hashed during packaging, but no trigger path covered it — so the CRLF fix for that check would not have re-run the check. Add the resources, verifier and .gitattributes paths that can break packaging. Co-authored-by: Orca <help@stably.ai> * perf(plugins): rebuild the panel frame only when its baked theme values change The revision keys the panel iframe, so every bump destroys the sandboxed frame and its in-panel state. It counted root attribute mutations, but --workspace-sidebar-live-width is written every rAF of a sidebar drag, so dragging with a panel open blanked it ~60x/sec. Compare the two values the shell actually bakes in instead. Co-authored-by: Orca <help@stably.ai> * test: stop pinning a plugin name in the CRLF guard The CRLF case rewrites every launch file, so the reported mismatch is whichever plugin sorts first. P2 adds theme plugins that sort ahead of orca-navigation-shortcuts, which broke the assertion there. Co-authored-by: Orca <help@stably.ai> * style: drop stray blank lines left by the rebase resolutions Both sides of the agent-hooks and orca-runtime conflicts contributed a trailing blank, which oxfmt rejects. Whitespace only. Co-authored-by: Orca <help@stably.ai> * test(plugins): stop the startup budget failing on machine load P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite parallelism, so the gate flaked. Widen it to catch an order-of-magnitude regression instead; the no-worker/no-plugin-code assertions are the real guarantee. Verified a 400ms regression still fails. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
7dab1e86e2 |
perf(ssh): normalize watch event paths once per fs.changed batch (#10881)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
58ef46d252 | lint: guard the two perf bug shapes we fixed repeatedly (#10851) | ||
|
|
8bee4bc62c | perf(source-control): share one path collator across the projection (#10850) | ||
|
|
62158570e0 |
perf(mobile-sync): memoize the agent-status projection per entry (#10787)
* perf(mobile-sync): memoize the agent-status projection per entry buildRuntimeMobileAgentStatusProjection re-serialized every live agent on every status ping. setAgentStatus replaces one entry and re-spreads the map, which defeats the reference-equality skip gate, so each ping paid for every other agent's prompt, 20-entry history, and 8 KB assistant message to discover they had not changed. Memoize each row's JSON by entry identity, the cachedTabsProjection pattern already used a few functions above. Per ping: 0.18 ms -> 0.014 ms at 8 agents, 0.88 ms -> 0.063 ms at 40. The output is byte-identical — joining pre-serialized rows matches whole-array stringify, which the new test pins against a verbatim copy of the old implementation. * test(perf): stop inflating the projection benchmark baseline The pre-fix arm stringified each row and parsed it back before stringifying the array, a per-row roundtrip the original never paid. That made the baseline artificially slow: the reported 5.9x-14.0x is really 2.1x-5.0x. Share one row builder between both arms, and check equivalence after a ping as well as on the cold call — a stale-row bug can only surface once the cache is actually exercised, which the cold-path check could never catch. |
||
|
|
5a30c5c2ed |
perf(git): read both diff blobs concurrently (#10781)
* perf(git): read both diff blobs concurrently The diff loaders awaited their two sides in series, so the second `git show` could not start until the first had returned. The reads are independent, so that was pure added latency on every diff the review panel opens: ~47 ms sequential vs ~24 ms concurrent, a saving of ~23 ms per diff. Covers the merge-base, commit, and staged loaders, plus the unstaged path where the working-tree read is independent of the index->HEAD chain. The unstaged left chain itself stays sequential because its second step depends on the first. The staged coalescing test asserted the sequential shape (one spawn, then the next); it now pins the contract that actually matters — eight identical reads still collapse to two spawns, one per side. * test(perf): interleave the diff-blob benchmark arms Running one strategy's whole batch before the other's lets cache warming, CPU frequency drift, and background load correlate with the strategy being measured. Alternate the arms per iteration, alternate which goes first, and report medians so that drift stays common to both. Also reject malformed env settings rather than truncating them — Number.parseInt accepts "10foo" and 3.5. Interleaved result confirms the original: 1.90x-2.03x, ~24 ms saved per diff. |
||
|
|
4109f4eec5 |
perf(agent-status): scan Command Code transcripts backward from EOF (#10742)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
c8e4488479 | perf(terminal): bound the PR-link carry scan to the trailing window (#10741) | ||
|
|
b168f6f100 | perf(agent-status): validate hook payloads without the JSON round trip (#10752) | ||
|
|
01f1e5e94e |
perf(renderer): give owner-routed settings a stable identity (#10743)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
9042ef9792 |
fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default (#10588)
* fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default Zellij and other multiplexers copy via OSC 52. Empty Pc is a valid XTerm default for clipboard, but we rejected it, and the feature defaulted off so copy silently failed inside Zellij. Accept empty Pc as clipboard, default the setting on (query still blocked; size capped), and surface Zellij in settings. Closes #10567 * fix(review): make the OSC 52 default actually reach existing installs Review fixes for #10588: - Persistence: profiles saved under the old off default persisted `false`, which is indistinguishable from a real opt-out, so the default flip never reached #10567's reporter. Added the repo's one-shot stamp (terminalAllowOsc52ClipboardDefaultedOnForAllUsers) so unmigrated profiles flip once and a later opt-out sticks. - Replay: reattach/cold-restore re-writes recorded PTY bytes through the same parser, so a stale `\e]52;c;...` silently clobbered the clipboard on every restart. Gated behind isPaneReplaying via a new resolveOsc52ClipboardGate. - Blocked toast latches once per renderer session and could be burned by a pre-hydration read; it now fires only for a real opt-out. - An empty Pd decoded to '' and, with the gate default-on, silently blanked the clipboard. Now rejected as invalid. - Localization: en.json is bundled and the catalog beats the code fallback, so all three copy changes were inert. Resynced across five locales. - Corrected the empty-Pc rationale: tmux (not Zellij) emits `\e]52;;<b64>`. * test(terminal): cover the OSC 52 gate wiring and settings copy Extracts createOsc52OscHandler so the replay/hydration gate wiring is covered, not just the pure gate — dropping the isReplaying getter now fails a test instead of passing silently. Adds catalog assertions for the two OSC 52 settings strings. Only the toast key was pinned, so the same inert-copy regression (code fallback edited, bundled en.json not) could still ship for the settings pane. * docs(settings): note that the OSC 52 default only covers new profiles Co-authored-by: Orca <help@stably.ai> * fix(terminal): migrate the web settings store to the OSC 52 default-on flip The default-on flip only reached the Electron store. The web/remote client keeps its own settings in localStorage, so a profile that persisted the old `false` there stayed opted out — the same bug the Electron migration fixed, in the second store. Extract the migration into shared/osc52-clipboard-settings.ts and call it from both stores. Also coalesce OSC 52 writes onto a microtask so a hostile chunk of ~15-byte sequences cannot fan out into a million clipboard writes, and latch the blocked-write toast after it renders rather than before. * feat(terminal): tell users when the OSC 52 flip overrides their opt-out The default-on migration cannot distinguish a deliberate opt-out from a profile that simply never touched the setting — both persisted `false` under the old default. Flipping everyone is the only way to fix #10567 for existing installs, but doing it silently reverses a security choice the user made. Arm a one-shot notice at load when the migration overrides a persisted `false`, on both settings stores, and show it once the renderer hydrates. Profiles that never opted out are never notified. * fix(terminal): clear the OSC 52 notice after it renders, not before Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the web OSC 52 notice armed against an unmigrated host The host store always projects osc52ClipboardDefaultOnNoticePending, so the plain spread in the web client's runtime UI merge overwrote an arm raised by its own localStorage settings migration — flipping the opt-out in silence. Co-authored-by: Orca <help@stably.ai> * fix(terminal): stop the OSC 52 notice overclaiming, and cover it Round-3 review fixes: - Rename the arming predicate to osc52ClipboardDefaultOnOverridesPersistedOff. Both stores rewrite the whole settings object on every save, so every profile saved under the old off default holds `false` — the deliberate-opt-out cohort is not distinguishable on disk. Name, docs and test names now say so. - Read settings before the UI snapshot in readLocalWebUIState: getStoredSettings() arms the notice, so reading first snapshotted a pre-arm state that callers wrote back, erasing an arm the stamp can never raise again. - Give the notice toast a stable id; StrictMode re-runs the effect against the same closure, so the early return cannot catch the second pass. - Restore guardParserHandler parity in the coalescer microtask. - Drop the unverified Zellij claim justifying all-selections routing; that routing predates this branch and PRIMARY routing stays an open question. - Cover the notice hook (order, single-fire, deep-link), the armed flag reaching disk and surviving a clear, and pin the notice catalog to its code fallbacks. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin OSC 52 setting discovery by product name The migration notice says to turn it off in Terminal settings, so searching Zellij/Grok/tmux has to find it. Also note why the OSC 52 write-back clauses stay despite an unrelated always-true clause in the same condition. Co-authored-by: Orca <help@stably.ai> * fix(terminal): consume the OSC 52 notice on close, and cover the guards it relies on The notice was cleared the moment the toast was enqueued, so a quit inside its 15s window spent the profile's only warning on a launch where nothing was ever seen — and the settings stamp means it can never re-arm. Clear on onAutoClose/onDismiss instead, plus explicitly in the action handler, because sonner's action path deletes the toast without firing onDismiss. Also closes three coverage gaps a review found: - ui.set must accept osc52ClipboardDefaultOnNoticePending. The update schema is strict, so dropping the key rejects the whole call rather than stripping it, and the renderer only logs that failure — every paired client would re-toast forever with nothing red. - the coalescer's try/catch and .catch had no test; the rejection case needs a plain function because vi.fn tracks settled results and hides the leak. - pin that every selection kind (including bare `p`) lands in the system clipboard, so routing PRIMARY separately later is a deliberate break. Co-authored-by: Orca <help@stably.ai> * test(web): pin that ui.get arms the OSC 52 notice when it runs the migration readLocalWebUIState reads settings before the UI blob so the migration's arm is in place before the snapshot every caller writes back. Seeding localStorage after install is what makes ui.get the first settings read, and therefore what makes swapping those two lines fail. Co-authored-by: Orca <help@stably.ai> * test(store): cover the OSC 52 notice clear and its hydration The clear sets local state before persisting so a rejected ui.set cannot leave the toast re-firing for the rest of the session; losing the persist only re-arms the notice next launch. Co-authored-by: Orca <help@stably.ai> * docs(terminal): state the real residual risk of default-on OSC 52 Three comment corrections from review: - the safety note claimed exfil was the risk; queries are blocked, so it isn't. The actual accepted risk is execute-on-paste: decoded text goes to the clipboard verbatim, newlines included. Filtering here would break multi-line TUI copies, which is the feature; bracketed paste is where that is handled, and kitty/Ghostty take the same posture. - the coalescer bounds a flood per parse yield, not overall. - the replay gate reads at parse time while queued live bytes are drained before the guard engages, so a copy racing a reattach is dropped silently. Co-authored-by: Orca <help@stably.ai> * test(terminal): close the four OSC 52 gaps a full revert walked through Mutation testing found four assertions that stayed green against the very change they were written to pin. The notice suite passed 8/9 against a complete revert to clear-at-enqueue: `calls[0][1][callback]?.()` is a silent no-op when the option is absent, and the call count was already satisfied by the enqueue-clear, so nothing separated "cleared by this callback" from "cleared earlier". Assert the option exists and the notice is unspent before invoking it. The stable toast id was deletable with all 9 green despite the adjacent comment calling it load-bearing for StrictMode. Pin it. The blocked toast's latch-after-throw fix was unproven: both orderings pass when `toast.info` succeeds. Only a throwing first call tells them apart. Deleting the hook call in App.tsx silenced the desktop notice with every suite green. Pin it alongside the static Toaster import, since sonner drops a toast enqueued before any Toaster subscribes and never replays it. Also retone the coalescer-latch comment, which claimed the reset ordering was load-bearing on its own; the try/catch reaches the same end, so the test binds the pair. All four verified green->red by mutation, then restored. * test(terminal): cover the OSC 52 notice and its guards Add tests pinning the static Toaster mount required to prevent notice dropout (#10567), the stable toast ID deduping StrictMode double-invokes, that the notice stays unspent on toast throws, and that flush-latch guards prevent silent consumption across error boundaries. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
fc513233cb |
fix(release-cut): gate an explicit RC against its own series (#10525)
* fix(release-cut): gate an explicit RC against its own series
semver_gt compares through strip_pre(), so the explicit-version override
only ever checked the stable line: 1.4.156-rc.0 read as 1.4.156, cleared
a 1.4.155 stable, and republished an RC below what clients already run.
Anchor a prerelease request on highest_rc_for_base -- the same rc history
the kind path uses -- so the override can only advance the series.
Two sibling gaps in the same block:
- version_suffix was silently dropped when version was set, because the
append lives in the kind branch the override skips.
- the shape regex rejected X.Y.Z-rc.N.suffix, so a suffixed RC the rc
path can produce could never be re-cut explicitly.
* fix(release-cut): close both ends of the rc-number range the gate compares
The new explicit-rc gate compares with `[[ -le ]]`, i.e. bash machine-width
integers, and the author closed only the low end. Past INTMAX bash saturates,
so `version=1.4.156-rc.99999999999999999999` reads as "above the published
rc.3" and the gate falls open — then the tag it cuts pins
highest_rc_for_base at 1e20 for that base forever, and every later cut wraps
to a lower rc the fleet never updates to. Bound the rc number to nine digits.
Also reject leading zeros on an all-digit prerelease identifier. `npm version`
renormalizes rc.4.01 to rc.4.1 while the tag step keeps the literal input, so
the shipped package.json version and its own release tag name different
releases. The explicit path's embedded identifier now goes through the same
validator the kind path uses instead of only the shape regex.
* fix(release-cut): stop the refusal pointing minor/major RCs at the wrong series
kind=rc derives its base from bump(latest_stable, patch), so the remedy the
refusal suggested only works when the requested base *is* that next patch. A
1.5.0-rc.N series exists only because this override created it, so an operator
resuming a stuck 1.5.0-rc.2 was told to dispatch kind=rc, which would have cut
an unrelated 1.4.156-rc.4. Spell the condition out and give the fallback that
does work for a non-patch base.
Also correct the mechanism in the comment I added in
|
||
|
|
7a01910f20 |
fix(skills): advance the release ledger at the cut so shipped revisions freeze (#10483)
* fix(skills): advance the release ledger at the cut so shipped revisions freeze #10340 made the released-skill registry a function of the committed ledger instead of a git tag walk, and #10460 reverted the cut step that advances that ledger because it violated the #9119 contract (a version-only cut must not regenerate or stage the content-addressed skill artifacts). Both were right; the result is a ledger that never advances. generate-skill-bundle-manifest.mjs:390 derives releasedCount solely from release-mapping.json and :461 assigns a changed skill releaseRevision = releasedCount + 1, while :518 protects only committedReleasedCounts[name] — so index releasedCount is unprotected. A tag ships that tail revision, nothing records it, and the next skill change rebuilds the same revision number over different bytes. Installs carrying the shipped digest then match no snapshot and degrade to unrecognized, which cannot be updated. Restore the advance in a form the #9119 contract can keep enforcing: --release now verifies that current-manifest.json and snapshot-registry.json already match the ref being tagged, appends the mapping row, and writes only release-mapping.json. The cut stages just that file, so it still cannot move a content-addressed artifact — the failure #9119 guarded against — and now fails loudly instead of recording a revision the tag does not ship. The contract test is narrowed to match: it asserts the cut runs --release (never --write) and stages exactly package.json and release-mapping.json. * test(release-cut): close the staging bypasses the narrowed gate left open The narrowed contract test anchored its `git add` scan to line start and only inspected staged paths, so three ways to reintroduce #9119 stayed green: a `git add` chained after `&&`, a write that never calls `git add` at all, and `pnpm run generate:skill-bundle-manifest` — the package.json alias for `--write`, which the hyphenated ban never matched. That last one also passed the pre-#10460 assertions, so it was never covered. Drop the anchor, require every `resources/skills` mention in the step to be exactly what is staged, and ban the alias and `commit -a`. Comments are stripped first so prose cannot trip a ban. Verified each bypass fails and the real workflow passes. * fix(release-cut): make the new provenance failure actionable to an operator Verifying the content-addressed artifacts is the only new way the cut can block, and it fails inside a step named "Bump package.json and tag" with a lint-shaped message. That names the files and the command but not the two things the operator needs: the regeneration has to land on main, and the cut is safe to re-run afterwards. Say so. Also pin down why assertReleasedHistoryPreserved takes the pre-append mapping. It pairs with artifacts.releasedSnapshotCounts, which seeding fixed before the row existed; handing it the post-append mapping makes every cut throw "Released snapshot history is incomplete", which points at tag fetching rather than the real cause. Nothing enforces the pairing. * test(release-cut): gate the whole cut job, not just the bump step Round-2 review defeated the previous gate twice, both proved by running the full contract file green with #9119 reintroduced. Every step in the cut job shares one workspace and one index, but the contract test only inspected `Bump package.json and tag`. A step inserted earlier could run --write and `git add resources/skills`, and the bump step's own commit swept it into the version commit and the tag. Assert job-wide instead: only the bump step may name the directory, and no step may regenerate under either the flag or its package.json alias. That lives in the generator suite because the contract file is at its max-lines cap. Two regexes were also evadable. The mention scan required a trailing slash, so a path held in a variable was invisible; it now matches the directory itself. The `commit -a` ban matched nothing at all — `commit\s` ate the only separator, so `-a`, `-am`, and `--all` all survived while only a trailing `-a` was caught. `--allow-empty` stays allowed. * fix(release-cut): assert the index, not the workflow text, before committing Round-3 review defeated the job-wide grep three ways, each proved by running both test files green with #9119 reintroduced into the tagged commit: an `env:` block holding `--write` and `resources/skills`, a composite action whose steps the workflow never spells out, and plain shell concatenation (`root=resources; leaf=skills`). Grepping shell source for path literals is inherently evadable, and the previous fix only relocated round-2's variable-indirection hole one step over. Move the invariant to where it cannot be dodged: immediately before committing, the cut diffs its own index and refuses anything that is not package.json or the release-mapping row. That does not care which step staged what, or how the path was spelled. The workflow grep stays as a cheap tripwire for literal spellings, now paired with a positive assertion that the index guard exists and precedes the commit — indirection cannot hide a missing guard. Mention matching dedupes and trims quotes, since the guard names the row a second time. * fix(release-cut): match the staged-path allowlist literally `grep -vx` treats its patterns as regexes, so the `.` in `package.json` matched any character: a staged `packageXjson` or a `resources/skills/release-mappingXjson` was silently accepted by the index guard. Verified both slip through `-vx` and are caught by `-vxF`. Exercised the guard against a legitimate cut, an empty index, a staged content-addressed artifact, paths containing a space and a non-ASCII character (git quotes the latter, so it fails closed), and a staged deletion. Only the two allowed paths pass. * test(release-cut): assert the index guard aborts, not just that it exists The positive assertion pinned the guard's shape and its position before the commit, but not its effect: replacing `exit 1` with `:` left both test files green while the cut logged the error and shipped the artifact anyway. That is the same failure this whole gate keeps having — asserting the shape of a defense rather than what it does. Pin the abort too. Verified the neutered guard now fails the suite. * test(release-cut): scope the abort check and catch clustered commit flags Two holes in the guards this PR added, both in the same shape-not-effect class the previous commit was meant to close. The abort assertion's lazy match was not scoped to the guard's own block, so it could borrow an `exit 1` from any later `if ... fi` in the step. Degrading the guard to a warning while adding a plausible HEAD precondition left every test green. Stop the match at the guard's `fi`. The `commit -a` ban only matched when `a` led the flag cluster, so `-vam`, `-va`, `-qam` and `-sam` all survived. That matters more than it looks: `commit -a` stages at commit time, after the index guard has already inspected a clean index, so it is the one way to defeat that guard. Match `a` anywhere in a short-flag cluster; `--allow-empty` and `--amend` stay allowed. Verified both mutants now fail. * fix(release-cut): validate the commit, not the index, before tagging The index guard asserted the wrong thing. `git commit` has a family of forms that commit the working tree rather than the index — `-a`, `-i`, `--only`, and a bare pathspec — so a rogue earlier step could leave regenerated artifacts unstaged and any of those forms would carry them into the tagged commit while the guard saw a clean index and passed. Reproduced end to end: `git commit -i resources` put current-manifest.json and snapshot-registry.json in the tag with all gates green, and `--only resources` additionally dropped package.json from the tag. Banning those flags one by one is the same enumeration game the earlier rounds kept losing. Assert the outcome instead: after committing and before tagging, diff-tree HEAD and refuse anything that is not package.json or the release-mapping row. That is indifferent to which step staged what and to how the commit was spelled. Verified the whole family is now blocked (-i, --only, -a, -am, -vam, pathspec, and an alias expanding to `commit -i`), that a stock commit and an --allow-empty re-cut still pass, and that deleting, neutering, un-anchoring, or relocating the guard each fails the suite. * fix(release-cut): make the commit guard fail closed on a merge commit Plain `git diff-tree` prints nothing for a merge commit, so the guard would have passed silently instead of failing closed — the one direction that matters on a release path. `-m --first-parent` reports the diff against the first parent; verified byte-identical output for an ordinary commit and still empty for the `--allow-empty` re-cut, so nothing else changes. Not reachable today (nothing in the cut job creates a merge, and npm version has no lifecycle hooks defined), but the failure mode is a guard that looks like it ran. Pin the flags in the assertion too, so neither dropping -m nor slipping in a `--diff-filter` can weaken it without failing the suite. |
||
|
|
8d61d76a59 |
fix(skills): decouple skill-manifest verify from local git tags (#10340)
* fix(skills): source released history from the committed ledger, not a tag walk verify:skill-bundle-manifest rebuilt the entire released-skill history by walking every local refs/tags/v* on each run and demanded byte-equality with the committed artifacts. Output was therefore a function of (skill bytes x local tag set x release timing), so any clone holding stray, deleted, or fork tags the committed artifacts predate rebuilt a divergent registry and failed lint. This was the 4th instance of one failure class (#8637 -> #9119 version bumps -> #9778 new tags -> local tag drift), each patched with a new tolerance rather than removing the tag coupling. Fix: the committed snapshot-registry + release-mapping ARE the released history; trust them instead of re-deriving from tags. - releasedHistoryFromCommitted() seeds generation from the committed ledger, dropping the floating unreleased tail (entries beyond what the mapping names). verify and --write are now pure functions of working-tree bytes with zero tag access. The tag walk survives only behind --rebuild-from-tags (disaster recovery), off the everyday path. - --release <version> + appendReleaseRow() perform the O(1) append of one mapping row at release cut (dedupes vs the last row, strips the v-prefix) -- the single authoritative point where working-tree bytes become an immutable released revision. - release-cut.yml runs generate --release "$VERSION" before the release commit (Node built-ins only, no install needed); pr.yml drops fetch-depth: 0 from the lint job since verify no longer needs tag history. Recognition is unaffected: the runtime uses knownSnapshots = registry.skills (all entries, incl. the tail committed at PR-merge time), so a missing mapping row only loses a version label, never recognition or the update nudge. Trade-off: lint no longer cross-checks committed historical snapshots against tags. A hand-edit to an old released entry is still caught by the runtime manifest<->registry consistency check when the current manifest points at it, and can be audited anytime with --rebuild-from-tags. Verified: verify passes committed-sourced; --write is zero-diff (byte parity); a planted stray v-tag no longer changes output; edit-stub -> --write -> --release appends the correct single row; double --release is idempotent; --rebuild-from-tags reproduces the committed artifacts. Generator tests 14 pass/ 1 skip; runtime skill-bundle-artifacts + freshness-inventory 14 pass; bundled skill guides verify passes. * fix(skills): keep one release-mapping row per version on a re-cut A cut that pushed the version bump to main but died before pushing the tag is re-cut at the same version. If skills changed in between, the second --release appended a duplicate row, and the stale one named revisions that tag never ships — which verify-skill-update-roundtrip then pairs with the tag's real bytes. Overwrite the trailing row instead (the tag is absent, so that version was never published). Refuse only when an earlier row claims the version, which the cut workflow already rejects upstream, so this cannot wedge a recovering cut. |
||
|
|
fc181a8496 |
fix(i18n): restore count separators in terminal theme picker for CJK locales (#9935)
The theme picker count row concatenates "Showing {count}" directly with
the " of {{value0}}" fragment. The ko/ja/zh translations dropped the
fragment's leading separator, so the shown and total counts fused
(e.g. Korean rendered "표시 중 3030 중" instead of "표시 중 30/30").
Restore a slash separator for the total-count fragment and the leading
space for the search-match fragment in ko/ja/zh, in both the runtime
catalogs and the key-override sources so catalog regeneration keeps the
repaired values. Add a regression test covering both fragments.
🤖 Generated with Claude Code
|
||
|
|
48a258d502 |
fix(win): stop shipping duplicate broken orca.cmd shim in app.asar (#9123)
The Windows CLI shim is delivered via extraResources to resources/bin/orca.cmd, beside the native resources/bin/orca.exe, and resolves the launcher adjacent to itself (%SCRIPT_DIR%orca.exe) — which works. But nothing in `files` excluded resources/win32/, so its source copy was also packed into app.asar and then extracted by asarUnpack:['resources/**'] to app.asar.unpacked/resources/win32/bin/orca.cmd. That duplicate has no adjacent orca.exe, so invoking it fails with "Unable to locate the native Orca CLI launcher", breaking orchestration skills that reach for the unpacked shim. Exclude the win32 shim source tree from app.asar so only the working extraResources copy ships. Add a regression guard to the electron-builder config test. Closes #7351 |
||
|
|
fde063618b |
fix(remote): create paired agent sessions without host focus (#10193)
* fix(remote): create paired agent sessions without host focus * test(remote): assert structured resume request * test(remote): preserve provider-separated resume coverage * test(remote): assert paired agent focus authority * fix(remote): separate agent host creation from viewer focus * test(remote): harden agent-session authority validation * test(remote): validate retired pane identity --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
aab112933e |
Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
87c59dd27d |
fix(claude-accounts): quote resolved claude path for Windows shell spawn (#10237)
* fix(claude-accounts): quote resolved claude path for Windows shell spawn runClaudeCommand spawns the resolved claude command with shell:true on Windows, but spawn concatenates the command into the cmd.exe line without quoting. When the CLI resolves to a path containing spaces (e.g. C:\Users\First Last\AppData\Roaming\npm\claude.cmd), cmd.exe splits at the first space and account add fails with: 'C:\Users\First' is not recognized as an internal or external command Quote the command the same way claude-pty.ts and quoteWindowsCmdArg already do for other Windows spawns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(claude-accounts): own Windows cmd invocation --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
8f40ddf328 | fix(memory): bound OOM-prone accumulators (#10179) | ||
|
|
56101422d2 |
fix(release): regenerate Windows blockmap via app-builder-lib JS (#10110)
electron-builder 26 dropped the app-builder-bin Go binary, so the signed-installer staging step failed with 'node_modules/app-builder-bin/ win/x64/app-builder.exe is not recognized'. Blockmap generation now lives in app-builder-lib's pure-JS buildBlockMap; call it through a small script in both the release-cut and signing-rehearsal workflows. Co-authored-by: Orca <help@stably.ai> |
||
|
|
a0944cc129 |
fix(linux): restore Ubuntu 20.04 launch — pin node-pty glibc symbols + add glibc/libstdc++ packaging gate (#9902) (#10019)
* fix(linux): restore Ubuntu 20.04 launch by pinning node-pty glibc symbols (#9902) The bundled node-pty pty.node is compiled from source in release CI on ubuntu-latest (glibc 2.39). glibc's 2.32-2.34 libpthread/libutil merge relocated openpty/forkpty (GLIBC_2.34) and pthread_sigmask (GLIBC_2.32) into libc under new symbol versions, so the from-source build bound to versions absent on Ubuntu 20.04 (glibc 2.31). The main process imports node-pty at startup, so the app crashed on launch. pty.node is the sole blocker (Electron needs GLIBC_2.25; other native modules <= 2.17). - Patch node-pty: a .symver shim pins the 3 symbols to their pre-merge version (GLIBC_2.2.5 x64 / GLIBC_2.17 arm64), and Linux-only ldflags force libutil.so.1/libpthread.so.0 back into DT_NEEDED. Guarded to Linux; macOS/Windows untouched. - Add a packaging gate (verify-linux-glibc-floor.cjs, afterPack): reads each bundled native binary's objdump -p version needs and fails the Linux build if any strong GLIBC_/GLIBCXX_/CXXABI_ node exceeds stock Ubuntu 20.04 (glibc 2.31 / GLIBCXX_3.4.28 / CXXABI_1.3.12). Catches GLIBC_ABI_DT_RELR, rejects GLIBC_PRIVATE, skips weak needs, fail-closed. - Docs + tests; the lazy sherpa-onnx speech prebuilt (GLIBCXX_3.4.29, never loaded at launch) is a documented libstdc++-floor exemption. * fix(linux): assert DT_NEEDED provider deps in the glibc-floor gate Harden the packaging gate (flagged in adversarial re-eval): the version-floor check alone can false-pass if the patch's forced `-l:libutil.so.1` ever silently drops — the pinned openpty@GLIBC_2.2.5 still resolves from libc's compat alias at build time, but fails to load on Ubuntu 20.04 where openpty/forkpty live only in libutil. The gate now also asserts that any binary importing openpty/forkpty keeps libutil.so.1 in DT_NEEDED. Validated on a real symver-pinned .so with libutil dropped (now fails) vs. present (passes). Documents the recommended real-host smoke-test follow-up. |
||
|
|
0326594d52 | Update paired Orca servers from the active client (#9839) | ||
|
|
41751dd90d | fix(runtime): route HUB-owned SSH worktrees through owning runtime (#9994) | ||
|
|
1a9e819c40 |
feat(skills): land remaining hybrid stubs (#9846)
* feat(skills): land remaining hybrid stubs * fix(build): exclude skill stub sources from packages |
||
|
|
300ee19950 | fix(terminal): make remote workspace sleep converge (#9874) | ||
|
|
b232df732b | fix(terminal): make remote agent sessions host-authoritative (#9687) | ||
|
|
34c160442f | Fix headless Linux serve pairing readiness (#9785) | ||
|
|
ae12bb1292 |
fix(skills): preserve released history across new tags (#9778)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
a10a2ba53c |
feat(linear): add MCP-style save issue (#9670)
* feat(linear): add MCP-style save issue * fix(linear): harden save issue parity * fix(linear): close save issue contract gaps * docs(linear): bundle project discovery with save issue |
||
|
|
05c32c4757 | fix(runtime): isolate navigation across paired clients (#9664) | ||
|
|
88c78611b7 |
fix(ssh): patch node-pty helper in Windows relay (#9638)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e58de71f5e |
feat(codex): real-home routing + self-contained multi-account homes (#9501)
* feat(codex): backfill managed-home sessions into the real Codex home once per host Orca-launched Codex sessions currently land only in the Orca-managed runtime home, so the user's own `codex resume` picker and app history never see them (#4444, #8612). Backfill the managed sessions tree into the real ~/.codex/sessions/YYYY/MM/DD layout once per host: - hardlink first (one physical rollout log), copy as the cross-volume fallback; existing target files are always skipped, nothing in either home is deleted or moved - idempotent; per-file failures leave the completion marker unset so the next startup retries cheaply - JSONL audit log of every link/copy/failure under <userData>/codex-session-backfill/ - honors the custom Codex session source home override, mirroring the existing system->managed bridge WSL managed homes are distro-local and need an in-distro variant; that is a follow-up. * feat(codex): flag-gated system-default real-home routing scaffolding Staged internal flag (default OFF, no settings UI): route the SYSTEM-DEFAULT Codex account at the user's real ~/.codex instead of Orca's managed runtime home. Flag OFF is byte-identical to today; managed (multi-account) selections are unchanged in either state. Routing (flag ON + host system default = no managed account): - CodexRuntimeHomeService.prepareForCodexLaunch / prepareForRateLimitFetch return null so the PTY/env layer injects no managed CODEX_HOME and the rate-limit fetcher + auth-presence gate fall back to ~/.codex (the background poller stops spawning Codex against the managed home — the #5370 auth war). - buildPtyHostEnv strips only a nested-Orca-inherited Orca-owned override (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a user-set CODEX_HOME. Shell-ready re-exports already no-op without the marker. - The headless commit-message Codex path strips the same inherited override. Hook install for the real-home lane (append-last into ~/.codex/hooks.json, trust via the app-server client) lands with the trust plumbing; the managed hook install is skipped for this lane meanwhile. Credit @jellychoco (#8606) for the native-home routing direction. Depends on the codex trust-rpc-grant plumbing for the real-home hook installer. * fix(codex): strip the daemon-inherited Orca CODEX_HOME override for real-home routing The daemon spawns PTYs from its own inherited environment and honors only spawnOptions.envToDelete, so mutating the sparse env object was not enough to strip an Orca-owned CODEX_HOME the daemon already carries. Add the strip to envToDelete for both daemon host-spawn paths, preserving a user-set CODEX_HOME. Verified live via CDP against a sandboxed dev instance (flag ON): an Orca-spawned pane reports empty CODEX_HOME/ORCA_CODEX_HOME, so Codex resolves its own ~/.codex. Adds daemon-path unit coverage (strip Orca-owned, preserve user-owned, no-op when flag OFF). * fix(codex): harden one-time session backfill * test(codex): cover staged cross-volume install * feat(codex): app-server trust-grant client, capability cache, and grant ledger Short-lived codex app-server JSON-RPC client (hooks/list + config/batchWrite, the same pair the Codex TUI 'Trust all' flow calls), run in a bundled ELECTRON_RUN_AS_NODE entry so synchronous launch prep can block on it with a hard deadline and guaranteed child reap. Capability cache modeled on GitCapabilityCache, scoped per execution host (native vs each WSL distro), with a narrow unknown-method/missing-subcommand unsupported predicate. The grant ledger records verified grants so steady-state launches skip the RPC. * fix(codex): grant managed hook trust via codex app-server RPCs in install/refresh Host and WSL installs now grant trust for Orca's managed status hooks through codex's own hooks/list -> config/batchWrite -> re-list verify, scoped to exactly the managed entries; the previous computeTrustedHash lane is the unchanged fallback for incapable/erroring CLIs. getStatus and the removal paths recognize ledger-recorded codex hashes so drift between codex's real algorithm and the replica no longer misreports or strands trust. SSH remote install is untouched by design. * test(codex): cover app-server trust grant client, cache, ledger, and lanes * test(codex): cover commit-message real-home override strip/preserve Adds the two cases for the headless commit-message Codex env under real-home routing: a nested-Orca-inherited Orca-owned CODEX_HOME is stripped, and a user-owned CODEX_HOME is preserved. * test(codex): WSL grant-lane coverage — in-distro invocation and fallback parity * feat(codex): real-home hook installer trusted via the codex app-server grant client With the real-home flag ON and the system-default selection, install Orca's status hook into the user's real ~/.codex before any pane spawns: - entry APPENDED LAST per managed event: codex hook trust keys are positional (source:event:group:handler), so appending keeps every user entry's position and trust record intact; user entries and unknown top-level hooks.json fields are preserved verbatim - trust is granted exclusively through the codex app-server client (hooks/list + config/batchWrite, verified by re-list); Orca never writes [hooks.state] into the user's real config.toml itself - if the grant lane is unavailable (old binary, unsupported RPC, verify failure), the appended entry is rolled back byte-exactly and the host keeps the managed-home lane end to end (PTY env, rate limits, commit messages) via a lane gate on the runtime-home service - one-time pristine backup of the user's hooks.json under Orca's userData; a rolling .bak sits next to the file (existing atomic writer) - hook opt-out sweeps Orca entries from the real home and drops Orca-owned trust records; flag-off downgrade re-arms the existing legacy system-home sweep, which removes the entry and its trust keys cleanly - the legacy system-home sweep is suppressed only while the real-home lane owns ~/.codex/hooks.json, so managed installs cannot delete the entry * fix(codex): resolve the trust-grant entry without requiring electron The grant bridge is reachable from plain-Node CLI entries, where the plain-node entry guard rejects any chunk containing require("electron"). Resolve the bundled session entry from __dirname (root chunk and chunks/ layouts) with an app.asar -> app.asar.unpacked rewrite for packaged runs, instead of electron's app path APIs. * fix(codex): keep session backfill off main thread Use asynchronous, sequential filesystem operations for the one-time rollout backfill, and avoid repeated target-directory probes. Treat inaccessible managed session roots as retryable failures instead of writing a false completion marker. * fix(codex): harden app-server trust grant fallback * fix(codex): install cross-volume session backfill copies atomically On a real Codex home whose filesystem supports no hardlinks (exFAT/FAT, some network mounts), the staged cross-volume copy was installed with a non-atomic copyFile(..., COPYFILE_EXCL) straight into the final rollout-*.jsonl name. An install interrupted mid-copy (app quit, crash, ENOSPC during the deferred run) could strand a truncated rollout that the next run then skips as already-present, defeating the staging design's own guarantee that a failed copy never leaves a partial session behind. Install the fully-staged copy with an atomic rename instead, guarded by an existence re-check so it keeps the never-overwrite contract (and the rename source is the same immutable managed rollout, so any clobber would be byte-identical). Cover the no-hardlink-support target and an interrupted install that must leave no partial in the user's sessions tree. * fix(codex): resolve grant entry from __dirname so plain-node CLI entries stay electron-free The build guard rejects any electron require reachable from plain-node entries; the bridge now maps app.asar to app.asar.unpacked by string replacement instead of consulting electron app paths. CLI typecheck project lists the new trust-grant module graph. * fix(codex): harden trust grant reconciliation * fix(codex): restore trust config permissions on rollback * fix(codex): harden real-home routing cleanup and retries * fix(codex): preserve unicode trust RPC responses * fix(codex): preserve remote env and complete real-home cleanup * fix(codex): preserve real-home lane invariants * test(terminal): isolate replacement idle reset assertion * fix(codex): preserve real-home dotfile links * fix(codex): preserve verified trust grants across launch prep * fix(codex): preserve dangling config symlinks on rollback * fix(codex): don't revoke a just-granted WSL home on a false 'missing' probe The async wsl.exe canonical-path settlement could report the runtime home 'missing' immediately after a verified RPC grant (a false negative — codex had just written and re-listed trust there), which drove the reconciliation 'remove' branch to delete all six granted [hooks.state] tables, leaving a bare [hooks.state] the launching pane read as 'hooks need review'. A 'missing' settlement now revokes only when no successful install ran this generation; a genuinely moved home still resolves to a different path and reinstalls. * test(codex): model codex config/batchWrite faithfully on Windows The grant-lane stub simulated codex by calling Orca's upsertHookTrustEntries, which writes both separator variants for a Windows key (a fallback-lane compat shim real codex never does) — fabricating duplicate tables and whitespace the RPC path never produces, so the byte-stable and no-duplicate assertions failed on win32. Replace it with a single-variant, blank-line-separated writer that matches the real 0.144.x binary's output. * feat(codex): collapse duplicate session listings across Codex roots Backfilled/bridged rollouts are hardlinked into both the real ~/.codex and Orca's managed runtime home, so AI Vault listed each session once per root (#7521). Dedup candidates by rollout file name pre-parse and parsed sessions by session id post-parse, keeping the canonical root: host real home first (unprefixed resume), then the managed runtime home, then other homes. Applies to local, WSL, and SSH-remote scans. * feat(codex): background sqlite index heal for backfilled sessions Codex's own state-DB metadata backfill is one-shot, so rollouts hardlinked in by Orca's session backfill never become visible to Codex's DB-driven surfaces. Extract the app-server stdio JSONL transport into codex-app-server-session (shared with the trust-grant client) and add a bounded, resumable background pass that drives Codex's lazy indexing via thread/read per backfilled session: recent-first, batched onto one short-lived server per batch with small concurrency, ledger + marker so steady-state startups are a no-op, stop-aware on quit, and capability-aware on CLIs without the app-server surface. * fix(codex): preserve session identity during dedup heal * fix(codex): preserve user trust during real-home cleanup * fix(codex): harden real-home heal boundaries * fix(codex): fail closed on unsafe backfill install * fix: harden real-home hook cleanup * fix(ai-vault): preserve execution boundaries and reap children * fix(codex): narrow app-server unsupported detection * fix(codex): bound user hook trust rebase retries per host The rebase lane ran a codex app-server session on every launch prep while a host was stuck (CLI without app-server support, or keys hooks/list cannot match). Gate the transaction on the shared capability cache and add the same 5-minute transient cooldown the grant lane uses, so sweep and legacy-cleanup retries cost plain fs reads instead of a codex session per pane spawn. * fix(codex): enforce real-home resume and heal boundaries * fix(codex): establish real-home lane before cleanup * fix(codex): stop index heal before delayed spawn * fix(codex): protect symlinked rolling backups * fix(ai-vault): preserve resume env deletion through drag * fix(codex): strip inherited Codex homes on mobile real-home resume The mobile resume surface types a bare real-home codex resume into a freshly created pane, but never asked for CODEX_HOME/ORCA_CODEX_HOME deletion at pane spawn, so an agentDefaultEnv-pinned or daemon-inherited Codex home rerouted the resume away from the user's real ~/.codex while the same session resumed correctly on desktop. Share the deletion helper from the AI Vault resume builders and forward it through the mobile launch and session.tabs.createTerminal call. * fix(codex): gate session migration on real-home lane * fix(codex): stop session backfill after opt-out * fix(codex): keep session heal failures retryable * fix(codex): keep session migration state recoverable * fix(codex): retry republished missing session heals * fix(codex): preserve hook symlink trust path * fix(codex): disambiguate POSIX trust paths * fix(codex): align hook trust source paths * fix(codex): harden trust grant lifecycle * fix(codex): restore envToDelete on client invocation type after base reconcile * test(codex): type child.stdout as PassThrough for oversized-output write * Assemble RC: reconcile app-server transport API across PRs Unify on the object RPC surface from the index-heal transport (#8921) while preserving the default-home env strip (#8828) and the narrowed missing-app-server capability signal (#8847): adapt the user-hook-trust-rebase consumer + tests, port envToDelete stripping into the shared session, and route stderr classification through the canonical capability-signal module. * RC: enable system-default real-home routing by default (flag ON) Flip codexSystemDefaultRealHomeEnabled to default ON for this RC's staged rollout (a user can still opt out by setting it false, which stays byte-identical to managed-home behavior). This is the only intended behavior difference between the RC branch and the individual PRs. Updates the two tests that assumed the prior OFF default. * fix(codex): snapshot hooks.json bytes+parse in one read to close real-home clobber race The install/sweep/legacy-cleanup paths parsed hooks.json, then did a separate later read to capture the previous bytes for the pre-write generation guard. A concurrent save (second Orca instance or the user editing the file) could land between the parse and that second read and be silently overwritten. readHooksJsonWithRaw returns the raw bytes and parse from a single read so the guard compares against exactly what it parsed. Adds a regression test that mutates hooks.json mid-RPC and asserts the sweep aborts without clobbering. * fix(codex): sanitize managed account config trust * fix(codex): guard OAuth add for custom providers * fix(codex): persist outgoing managed tokens before real-home lane takeover (PR-C) prepareForCodexLaunch returns null early for the real-home / system-default lane before syncForCurrentSelection runs. If a managed account is still recorded as synced when the selection has dropped to the system default (nulled without a sync pass, or auto-deselect on missing managed auth), a Codex-refreshed token stranded in the shared runtime home is never persisted to its canonical per-account home -> token loss. Read the outgoing managed account's refreshed token back before the real home takes over. The real-home lane implies host === null, so running the managed->system-default transition restores only Orca's runtime mirror from ~/.codex and never writes the real ~/.codex. It is a no-op once the selection has already been reconciled, so the normal select path does not double-write. * fix(codex): preserve refreshes across all default transitions * feat(codex): show system-default/real-home account identity in switcher (PR-B) The account switcher modeled the system-default Codex account as activeAccountId:null with no identity fields, so the null row rendered blank ("System default" / generic subtitle) even though its effective login is whatever ~/.codex/auth.json currently is. Add a CodexSystemDefaultIdentity descriptor {hasAuth, authKind, email, providerAccountId, workspaceLabel} to CodexRateLimitAccountsState, resolved live and READ-ONLY from ~/.codex by the accounts service and returned from listAccounts()/getSnapshot(). The settings switcher now renders the null (system-default) row as that real identity: the OAuth email when signed in, "Custom provider — no usage tracked." for env-key/custom-provider logins (auth.json with OPENAI_API_KEY, or an OPENAI_API_KEY env with no auth.json), and the generic fallback when signed out. Identity is host-scoped (per-distro WSL keeps the generic label). Orca never writes ~/.codex; managed-account switches only touch Orca-owned homes, so the system-default identity stays a stable, displayed source of truth. Usage already routes to the real home via getSystemCodexHomePath, so the switcher now attributes it to a real face. Tests (sandboxed temp homes only): OAuth email/provider resolution, api-key auth.json and env-key (no auth.json) as custom-provider, signed-out, and select/deselect of a managed account never mutating ~/.codex/auth.json. * fix(codex): parse multiline provider pins in OAuth guard * fix(codex): harden managed trust sanitization * fix(codex): harden system-default identity rendering * feat(codex): give each managed account a self-contained CODEX_HOME; retire shared mirror (PR-E) With the real-home flag ON, a host managed account now launches directly against its own codex-accounts/<id>/home instead of the shared runtime mirror + auth.json hot-swap: - codex-home-paths: syncSystemCodexResourcesIntoManagedHome links system resources into any managed home (ownership-marker discipline; never symlinks into / mutates ~/.codex). - runtime-home-service: prepareForCodexLaunch / prepareForRateLimitFetch / syncForCurrentSelection route the per-account home directly and skip the shared-home hot-swap + token read-back; each home keeps its own auth in place (fixes GAP-5 concurrent auth race). Session discovery scans every per-account home. - hook-service / hook-trust-promotion: install/getStatus/refresh accept a runtimeHomePath so hooks + RPC-granted trust land in the per-account home. - service: config mirror into a self-contained home uses the trust- preserving merge so granted hook/project trust survives account switches. - codex-session-root-dedup: rank codex-accounts/<id>/home as canonical managed alongside the shared runtime home. Flag-OFF and the system-default real-home (null) lane are unchanged; the nested-Orca CODEX_HOME===ORCA_CODEX_HOME daemon strip (#5370) is preserved. Sandboxed tests only; ~/.codex is never mutated. * fix(codex): validate per-account home ownership * fix(codex): keep managed rollouts discoverable across real-home opt-out WI-4 lossless migration/rollback validation for pre-E shared-mirror managed accounts. Session discovery gated the per-account home scan on the real-home flag, so opting back out (flag OFF) hid every rollout an account accumulated while the flag was ON — the data stayed on disk but vanished from the AI Vault until the flag flipped back on. Scan a managed host home whenever it holds a sessions/ tree, independent of the flag; a never-enabled install keeps its homes credential-only so opt-out stays byte-identical to today. Forward migration was already lossless (the shared mirror is always scanned) and the opt-out credential read-back already refuses to overwrite a fresher per-account token; add tests locking all three invariants. Sandboxed tests only; ~/.codex is never touched. * fix(codex): migrate stranded shared auth on E takeover * test(e2e): isolate Electron from developer Codex home * test(codex): add real-account validation harness * fix(codex): finish C and E matcher composition * fix(codex): bound validation harness shutdown * test(codex): isolate hook lifecycle user data * test(codex): cover realistic account-home migration * fix(codex): keep standalone home tripwire active * test(codex): fingerprint system auth in validation reports * fix(codex): bind managed homes to account ownership * fix(codex): normalize Windows trust source identity * fix(codex): make Windows trust upgrade transactional * test(codex): use TypeScript pipeline for validation scripts * test(codex): run validation modules through native node * test(codex): allow slow Windows tripwire startup * fix(codex): survive lingering Windows codex login processes in add-account On Windows, codex login can keep running (with descendants) after it has written auth.json, holding OS handles on the per-account managed home (log/codex-login.log). That made doAddAccount's post-login cleanup fail with ENOTEMPTY (rmSync) and left an orphaned codex-accounts/<id>/home. - runCodexLogin now watches for auth.json on Windows and force-kills the login process tree (taskkill /t) if it lingers past a short grace period; the forced exit is treated as a successful login. The 120s timeout path also kills the whole tree instead of only the direct child. macOS/Linux behavior is unchanged. - safeRemoveManagedHome now removes homes with rmSync maxRetries / retryDelay (mirroring the local-worktree-filesystem Windows policy) and no longer lets a cleanup failure mask the original add error. - run-codex-real-account-validation.mjs accepts --temp-parent / ORCA_CODEX_VALIDATION_TEMP_PARENT so the disposable root can live outside %USERPROFILE% on Windows, and fails with an actionable message before creating anything when the temp parent is inside the primary home. The real-home guard is unchanged. * fix(codex): preserve managed-account MCP .credentials.json on per-account-home migration (#8440) Codex file-mode MCP OAuth tokens live in $CODEX_HOME/.credentials.json, keyed by MCP server URL with no account identity of their own. The legacy shared-mirror -> per-account-home migration only carried auth.json, so an existing managed account with authed MCP servers had its tokens stranded on upgrade and silently needed re-auth. Carry the shared mirror's .credentials.json into the same identity-proven per-account home alongside auth.json: only into the single uniquely-matched active account (no cross-account leak), only when the destination has none yet (never clobber a newer file the account authed in its own home), atomic 0600, absent-source no-op. New MCP auth already lands in the per-account home since that home is CODEX_HOME. * fix(codex): preserve Windows reauthentication login flow * test(codex): build real-account validation harness cross-platform on Windows The harness built its app with execFileSync('npx', ['electron-vite', ...]), but npx resolves to a .cmd shim on Windows that execFileSync cannot launch (ENOENT), so the harness could not build its own app there and required --skip-build with a prebuilt out/main/index.js. Extract resolveElectronViteBuildCommand(repoRoot): it runs the repository-local electron-vite JS entry (node_modules/electron-vite/bin/electron-vite.js) with the current Node binary (process.execPath), which resolves identically on macOS, Linux, and Windows with no shell. It throws a clear error if the local entry is missing (install deps or pass --skip-build). --skip-build behavior is unchanged. Add regression coverage asserting the build command uses process.execPath and the repo-local JS entry (not npx), and that a missing entry fails clearly. * fix(codex): version the MCP creds migration independently of the auth marker The auth carry and the MCP .credentials.json carry (#8440) shared one existence-only v1 marker, so any build that stamped the auth-only marker first would strand the MCP store forever. The MCP carry now concludes via its own per-account-mcp-creds-migration-v1.json marker and runs even when the auth marker is already present; ordering is code-enforced instead of landing-discipline-enforced. Also isolate per-account read failures: one stale or deleted account home no longer aborts the whole migration. The broken account stays in the unique-identity ambiguity gate via its stored fields but is never read or written, so the active account still migrates. * fix(codex): fail corrupt managed auth.json without echoing credential bytes A raw JSON.parse SyntaxError from loadOAuthCredentials could carry auth file fragments into logs and the add/reauth error surface. Throw a sanitized error instead; filesystem errors still propagate unchanged. * fix(mobile): give the pairing runtime a disposable home for the E2E boot guard The main-process guard now refuses to start with ORCA_E2E_USER_DATA_DIR set but the real user home, and this was the one caller not updated — the temporary pairing runtime crashed before emitting its pairing URL. * test(codex): canonicalize harness containment guards and retry cleanup Resolve symlinks before the disposable-root containment checks so a symlinked temp parent cannot smuggle the throwaway home inside the primary home, and give the final cleanup rm Windows retry/force so a briefly lingering codex handle cannot strand the credential-bearing root. * test(codex): add lane-aware containment mode to the real-account harness The Windows gate-D run proved strict zero-event whole-profile containment is structurally unreachable with the real-home flag ON: system-default spawn sites deliberately delete CODEX_HOME so native codex resolves the real ~/.codex, and on Windows the binary ignores the USERPROFILE sandbox. Its own volatile runtime churn (root sqlite/WAL/SHM, tmp/, log/) is the shipped Phase-1 design, not a candidate defect. --lane-aware-containment records those designed events without aborting while every other real-home write — auth.json, config.toml, .credentials.json, hooks.json, sessions/, anything unknown — remains a hard violation and still aborts the run. Default behavior is unchanged (strict); the absolute zero-event claim stays carried by macOS runs, where HOME does sandbox native codex. * test(codex): allow the real-account harness to pin the real-home flag off --system-default-real-home off seeds and env-pins the flag OFF so every codex spawn gets an explicit managed CODEX_HOME and native codex never resolves the OS profile. This is the only Windows configuration where the strict zero-event whole-profile tripwire is reachable, and it matches the stable-rollout default; flag-ON runs keep lane-aware classification. * test(codex): correct the flag-off harness comment to kill-switch rationale The rollout ships all codex-home changes at once (no phased rollout), so flag OFF is the emergency kill-switch lane, not the stable default. * test(e2e): canonicalize the isolated E2E home path The disposable HOME lives under os.tmpdir(), whose spelling is an alias on CI (macOS /var symlink, Windows 8.3 RUNNER~1). Git canonicalizes worktree paths, so worktrees created under the aliased home never matched the app's listing — golden core flows and the packaged crash-survival harness failed with 'worktree created but not found in listing'. Resolve the home to its canonical spelling at creation in both the e2e helper and the packaged-app driver. * fix(codex): address CodeRabbit review on the landing PR - carry envToDelete through the mobile agent-resume startup plan so a real-home Codex resume cannot inherit an ambient CODEX_HOME - strip Orca-owned Codex overrides in the commit-message WSL fallback, matching the host fallback - strip ELECTRON_RUN_AS_NODE in the computer-e2e driver like every other home-isolation caller - drop the unused hooksEnabled parameter from isRealHomeCodexHookLaneUsable * feat(codex): ship real-home routing unconditionally, remove the rollout flag The codexSystemDefaultRealHomeEnabled setting is gone from types and constants and the helper no longer consults settings — the system-default real-home lane and per-account homes ship for everyone in one release. This also un-strands profiles that rc-era builds stamped with false (the setting had no UI, so every stored false was a seeded artifact that would have silently kept those users on the legacy mirror forever). The ORCA_CODEX_SYSTEM_DEFAULT_REAL_HOME env override survives strictly as a test-rig control: the containment harness pins the legacy lane for strict zero-event Windows runs, e2e home isolation pins lanes inside disposable homes, and the legacy-lane test suites now route their per-test lane selection through it. --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> |