mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
5cec2c2dfcae0700ecfee5b7e1d67200bb74d375
571
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1924c8f5b1 |
feat(perf): lint repeated sort setup and schedule regression contracts (#18822)
* feat(perf): audit comparator setup and schedule performance contracts * test(sqlite): close readers after expected busy failures * ci(perf): trigger contract workflow on the contract files themselves Without these paths a contract rename lands green on PR CI and only breaks the next nightly, where nobody owns the failure. Also run the OS-independent source audit once instead of on all three runners. |
||
|
|
9c3d957ae3 | perf(automations): reuse collation setup for list name sorting (#18823) | ||
|
|
4c5077d57a |
perf(persistence): skip rewriting unchanged terminal scrollback snapshots (#18764)
* perf(terminal): tighten the partial-escape-tail benchmark and equivalence test
* perf(terminal): spell the ESC gate the same way as the sibling ingest gates
* test(terminal): differential-fuzz the ESC-free partial-escape-tail gate against the unguarded fold
* test(terminal): make the escape-tail fuzz exhaustive at symbol depth, and cap the fold expectation
Two review findings on the differential fuzz, both about the test faithfully modelling the
function it guards.
The odometer generated strings by symbol depth but the caller filtered on `chunk.length`, which
is the UTF-16 code-unit count. An astral symbol is two code units, so every depth-4 string
containing one was silently skipped and the corpus was not exhaustive at depth 4 the way the
test name claimed. The generator now yields `{ depth, text }` and the caller filters on depth.
That restores the missing strings and takes the pinned corpus from 516,566 to 593,468 - exactly
the count CodeRabbit derived for the intended corpus.
The pairing assertion in the sibling suite compared the capped `advancePartialEscapeTail`
against an uncapped `extractPartialEscapeTail(pending + chunk)`. It passed only because no
pairing in that corpus crosses MAX_PARTIAL_ESCAPE_TAIL_LENGTH; it would have stopped modelling
the function the moment one did. The cap now lives in the expectation, matching the fuzz
oracle.
Re-verified the fuzz still fails on a wrong guard: mutating the gate to a bracket check fails
all four tests with a `gate diverged` assertion on a lone ESC chunk.
Reported by CodeRabbit and pullfrog on #18748.
|
||
|
|
320f4c8b87 |
perf(renderer): index four projections that rescanned their inputs per keystroke (#18747)
Four renderer projections scanned a collection inside a loop over another collection, each on a
path that reruns per keystroke or per store write. All four now build the index once per stable
input, which is what the surrounding code already does for its other lookups.
`workspace-kanban-search.ts` called `searchWorktrees`, the convenience wrapper that builds the
palette document index inline. The board's filter hook memoized the whole call on the query, so
every character re-normalized and re-segmented every indexed field of every worktree — and did
it again on every agent-status tick while a query was active, since those churn board
identities. `buildWorkspaceBoardPaletteDocuments` splits out, memoized on
`[worktrees, repoMap]`; only the match reruns per keystroke. This is the shape
`worktree-jump-palette-document-index.ts` already provides for Cmd-J.
`useTabGroupItemProjections` resolved each editor tab against `state.openFiles` — the global
list across every worktree — and each `tabOrder` entry against the group's tabs, with a `.find`
per element. Both are now `Map` lookups, alongside the `terminalTabById` index the same file
already built. The memo key `groupTabs` gets a new identity on any unified-tab write, so this
ran on title, label and colour changes.
`buildSourceControlTree` rebuilt every ancestor path with `segments.slice(0, i + 1).join('/')`
per segment, making tree construction O(files x depth^2) in characters copied — on the path the
Source Control file filter rebuilds per keystroke. The path now accumulates.
`worktree-header-section-boundaries.ts` ran a full `findIndex` over the render rows for every
header row, plus an `indexOf` over the bucket ordering, in two `useMemo`s keyed on `renderRows`
— so it recomputed on every sidebar row-model change, not just during a drag. One indexing pass
each, first-match-wins to match `findIndex`/`indexOf`. The successor index is keyed per bucket
because a repo or group id can appear in more than one bucket ordering; a flat id-keyed map would
pick whichever bucket was iterated first. `worktree-header-section-boundaries.test.ts` pins that
and the first-wins duplicate-header case.
Measured by `pnpm bench:renderer-quadratic-scans`. Three scenarios time the production export
against a reproduction of the pre-change function; the tab-group scenario is modelled on both
sides because the projection lives inside a React hook. Each asserts before/after agree first.
| projection | drives | scale | before | after | |
| --- | --- | --- | --- | --- | --- |
| workspace board filter (per keystroke burst) | production | 300 worktrees x 12 keystrokes | 15.4 ms | 4.4 ms | 3.5x |
| tab-group projections (per unified-tab write) | modelled | 60 tabs x 120 open files | 1.23 ms | 0.34 ms | 3.6x |
| source-control tree build (per filter keystroke) | production | 5000 changed files | 6.2 ms | 3.9 ms | 1.6x |
| sidebar header boundaries (per row-model rebuild) | production | 80 repos x 600 rows | 2.0 ms | 0.8 ms | 2.6x |
The tree and sidebar wins are smaller than the scans they remove because the rest of each
function (tree finalize/sort, per-row size estimation) is linear and now dominates.
4,537 existing sidebar, tab-group and right-sidebar tests pass unmodified.
|
||
|
|
c36dd5f4c7 |
perf(terminal): skip the partial-escape-tail walk on ESC-free PTY chunks (#18748)
`advancePartialEscapeTail` runs once per PTY chunk, on the main thread, for every terminal — visible, hidden or parked — inside `HeadlessEmulator`'s write path. It unconditionally concatenated the pending tail with the whole chunk and then walked the result one code unit at a time through a VT500 state machine. `extractPartialEscapeTail` only leaves `ground` on an ESC byte, so with no pending tail and no ESC in the chunk the answer is always ''. Taking that case up front skips both the full-chunk concat and the walk. `String.prototype.includes` is a native scan, so the gate costs essentially nothing on the chunks it does not short-circuit. This is the same gate its two neighbours on the very same ingest path already apply — `TerminalOscCwdTitleScanner.scan` and `TerminalMouseModeMirror.scan`, both carrying a comment citing their measured share of a 2.2x ingest regression. This call was simply missed. Measured by `pnpm bench:terminal-partial-escape-tail` over 640 x 16 KB chunks (10 MB), median of 7 rounds: | stream shape | before | after | | | --- | --- | --- | --- | | ESC-free (build logs, `cat`, piped output) | 40.3 ms | 0.19 ms | 213x | | SGR-coloured output (gate does not apply) | 30.6 ms | 32.1 ms | 1.0x | The benchmark proves equivalence over a 226-case corpus before timing, and a new unit test pairs every pending-tail state the scanner can be left in against every chunk shape, asserting the gate is indistinguishable from the unconditional fold. |
||
|
|
e1599c94b8 |
perf(terminals): let idle panes share one process-table capture instead of forking their own (#18742)
* perf(terminals): let idle panes share one process-table capture instead of forking their own Every visible local pane runs an agent-completion cadence that resolves through `getStrictProcessTableSnapshot`, and the inspection queue already collapses every shared-observation task enqueued in the same tick onto a single whole-host `ps`. Independent ±10% jitter per pane defeated that: the jitter was re-rolled on each reschedule, so panes drifted permanently apart, each landing in its own tick and each missing the snapshot's 500 ms TTL. Four idle panes cost four captures where one would have served all of them. Idle panes now aim at a deadline grid anchored at the epoch. The pull-forward is clamped to the snapshot TTL, so no interval is ever longer than its tier and none is more than 500 ms shorter: a pane off the grid walks onto it over at most `tier / TTL` steps, costs at most one extra inspection in total, and no inspection is ever delayed. Scoped deliberately. A pane with a foreground agent, or one still inside the 10 s post-activity hot window, keeps its exact interval and its own phase, so the bounded hot cadence is unchanged. The error-backoff path keeps its jitter, where spreading retries across panes is the point. Measured by `pnpm bench:agent-inspection-cadence` — whole-host `ps` captures over 60 s at the 2 s idle tier, median of 21 rounds: | visible panes | before | after | reduction | | --- | --- | --- | --- | | 1 | 29 | 29 | 0% | | 2 | 42 | 30 | 29% | | 4 | 62 | 31 | 50% | | 8 | 82 | 32 | 61% | `process-table-snapshot-reader.ts` measures the `command=` column at 1.15 s of work for 1,948 processes, so these are captures a quiet app was paying for continuously. All 4,202 existing terminal-pane tests pass unchanged, including the no-evidence cadence suite that pins the relaxed and hot intervals. * test(terminals): report n/a instead of dividing by a zero baseline in the cadence benchmark A window shorter than one cadence tier leaves the baseline capture count at zero, and the reduction line then divided by it and printed a meaningless percentage. Reported by CodeRabbit on #18742. |
||
|
|
149732df6d | perf(persistence): stop the session write re-scanning and rebuilding unchanged state (#18739) | ||
|
|
b33d1972bc |
docs(relay): correct why the ConPTY teardown asset diverges from the desktop patch (#18636)
The divergence pinned by #18601 is real and worth keeping, but its stated reason was wrong. It claimed the desktop patch carries the early conin placement "and therefore the +2 File / +1 Process regression, measured against its exact installed tree" -- i.e. that the shipped desktop app leaks because of its own leak fix. It does not, for any terminal a user opens. node-pty defaults `_useConptyDll` to false. Every desktop site that opens a pane sets it true (`local-pty-utils.ts` twice, `native-pty-spawn.ts`), as does the `windows-conpty-warmup.ts` warm-up, so they take the `else` branch, where upstream already destroys the input socket. The relay passes no such option (`src/relay/pty-handler.ts`) and takes the `!useConptyDll` branch -- the one both this asset and the desktop patch edit. The desktop is not entirely off that branch, though: the hidden rate-limit probes in `src/main/rate-limits/claude-pty.ts` and `codex-pty-rate-limit-probe.ts` omit the option, recur, and tear down through `kill()`, so the hunk is live there -- just never for a visible pane. Whether the early placement costs the same +2 File / +1 Process across a probe's lifecycle is unmeasured; the numbers in this comment were taken on relay-style spawn/kill cycles, and the comment now says so. What is settled is the replaced claim: not every Windows user, and not every terminal. Those two probes were missed three enumerations running because they use `await import('node-pty')`, which no static-import grep finds. The comment now tells the next reader to grep for `node-pty` instead. The measurement that produced the wrong claim was taken by a standalone harness that passed no `useConptyDll` and so defaulted into the branch it was not trying to measure -- the same standalone-is-not-the-real-host trap #18601's own body warns about, one level down. Also refreshes the self-exit paragraph, which #18635 made stale. That leak is now fixed for the desktop, and the note records why the fix cannot reach a Windows relay. The fix is mostly native (`src/win/conpty.cc`) and this asset only rewrites `lib/*.js`, and all three delivery paths stop short of Windows: pnpm patches do not cross the SSH boundary; `MATRIX_SLOTS` in `build-orcad-prebuilds.mjs` has no win32 entry; and the one relay asset that does patch native source and rebuild on the host (`node-pty-1.1.0-master-cloexec-patch.cjs`) returns `skipped:unsupported-platform` for anything but linux/darwin. #18635's flat self-exit relay numbers were measured against a locally rebuilt binary, so they describe the relay code path on a patched tree, not the tree a relay host installs -- the note says so explicitly rather than leaving the next reader to conflate them. Assertion and hashes unchanged: the relay must still release conin after the console-list fork, and a patch sync must still not copy the early placement onto the relay's branch, where it does cost +2 File and +1 Process per terminal. Only the justification changes, plus the test name, which said "like the desktop patch" where it meant "unlike the desktop patch placement". |
||
|
|
41b520259e |
ci(package): retry apt fetches and docker builds behind the Ubuntu mirror (#18797)
The package job builds three Docker images whose apt-get update/install hit archive.ubuntu.com with no retry, timeout, or mirror fallback. When the mirror is mid-sync every build dies in one of three ways: - per-package fetch stalls (~64 s each, `Ign:` lines) until the runner's 10-minute docker build timeout fires: https://github.com/stablyai/orca/actions/runs/33935104546/job/101221447425 - `apt-get update` exit 100 with `Hash Sum mismatch` on noble-updates/restricted/Packages.gz: https://github.com/stablyai/orca/actions/runs/33935104546/job/101226099525 - `apt-get update` exit 100 with `File has unexpected size ... Mirror sync in progress?`: https://github.com/stablyai/orca/actions/runs/33935244026/job/101231083497 Each Dockerfile now retries `apt-get update` up to five times with Acquire::Retries and a 30 s HTTP timeout, clearing /var/lib/apt/lists between attempts so a half-synced index is never reused, and passes the same acquire options to `apt-get install`. Each runner script retries the whole `docker build` once when the first attempt fails or times out. |
||
|
|
0a821e5bc8 |
fix(crash-reporting): make the own-Chromium gate a real choke point, and stop a refusal leaking the root (#18459)
* fix(crash-reporting): make the own-Chromium gate a real choke point
Round-3 review found the guard was not the choke point its own comments
claimed: six pid-addressed `taskkill /pid <pid> /t /f` families in main were
ungated and uninstrumented, so the stale-pid shape stayed producible and a
`selfInitiatedTreeKillCount: 0` could read as exculpatory when it was not.
- Gate the remaining main-process families: the git command-runner abort, the
notebook-cell and automation-precheck timeouts.
- Turn the `src/shared` seam into the gate itself (`process-tree-kill-gate`), so
the runProcess choke point, the codex app-server deadline kill and the
ephemeral-VM recipe kill ask the same decision. Those three are compiled into
the CLI/relay too and cannot import main; main installs the guard at preflight.
- Ratchet (`main-process-tree-kill-gate.test.ts`): a new pid-addressed taskkill
in main that skips the gate fails, and the allowlist entries must still exist.
- Give pid-addressed kills eviction priority in the 32-entry ring: 32 routine
`win-pty-job` teardowns from a window-close burst no longer evict the one
entry that discriminates a self-kill from an external one.
- Correct the coverage doc, which described the uninstrumented Windows sites as
POSIX `process.kill(-pid)` group kills and omitted the git and codex paths.
* fix(crash-reporting): keep a refused tree-kill from leaking the root it owns
A refusal must block the pid-addressed tree walk, not the termination. Five of
the six gated sites returned on refusal with no fallback, so a refused
`taskkill /pid /t /f` left git.exe, a timed-out notebook cell, an automation
precheck or an ephemeral-VM recipe running while the caller reported it stopped.
The root kill is addressed by the child handle, which cannot reach the recycled
pid the refusal is about, so it stays correct and required on that path.
Also fixes the ring eviction the scope preference introduced: with the ring
saturated by pid-addressed kills, the only non-pid-addressed entry is the one
just pushed, so the splice evicted itself and the detail came back `{}` --
byte-identical to the external-kill arm, in the window-close case the guard
exists for. Eviction now excludes the newest entry and falls back to FIFO.
Tests: refusal now asserts the root kill at all six sites, and the ring covers
the saturated-pid ordering as well as round 3's group-burst ordering.
* fix(crash-reporting): stop a refused tree-kill leaking the commit-message agent, and count call sites
Two round-5 blocking findings, both open on main and on both branches.
`killSourceControlAgentProcess` had no root-kill fallback on its win32 arm: the
taskkill was the only termination, so once the own-Chromium gate could refuse it
the promise resolved having killed nothing. Both callers do
`terminationComplete ??= killSourceControlAgentProcess(child)` and then release
the managed-home lock on that promise, so a refusal left the local Codex/Claude
commit-message agent running while the caller reported it stopped -- the
lock-contention failure the taskkill was added for. Same fix as the six sibling
sites: the handle-addressed root kill cannot reach the recycled pid the refusal
is about, so it stays correct and required on that path.
The ratchet was file-granular, not call-site granular: one gate mention anywhere
in a file exempted every taskkill in it, which left the six files that now ask
the gate ratchet-blind -- the inverse of what it is for. It now counts `/pid`
call sites against gate admissions per file, so a second ungated kill inside an
existing family fails. Keying on the `/pid` argument rather than a quoted
`taskkill` also catches a kill whose program name comes from a constant. The
three comments that claimed more than the old scan enforced now state the rule
and its two remaining blind spots.
Also: the recording in `admitSelfInitiatedTreeKill` is now wrapped the way the
`admitProcessTreeKill` seam already wraps it, with the refusal decision taken
before anything that can throw so a diagnostics failure cannot flip it; and
`orca-chromium-process-pids` documents the false-positive direction (a stale
`getAppMetrics()` entry plus pid reuse refuses a live unrelated child), which is
the mechanism the root-kill fallback exists to bound.
Tests: refusal now asserts the root kill at all seven sites; the ratchet asserts
call-site counting and the constant-program form.
* test(crash-reporting): run the own-Chromium gate against real Windows trees
Nothing on this branch had ever executed on Windows. The unit tests pin the
gate's decision against a mocked taskkill, which cannot show that the decision
does anything to a real process: that `/T /F` reaps a detached grandchild, that
a refusal leaves that tree standing, or that the handle-addressed root kill the
refusal path falls back to reaps the root while orphaning descendants.
Adds a win32-gated live test covering all four, registered in both the
`package_windows` CI lane and `WINDOWS_PACKAGE_TESTS` as
`win32-test-lane-registration` requires.
Also completes the coverage doc's "never instrumented" list, which omitted the
macOS keyboard-input-source probe's POSIX group kill in `ipc/app.ts`.
* fix(crash-reporting): pin the commit-message root kill on the Windows arm
The first Windows run of this branch found nine failures the macOS suite
cannot see: `commit-message-text-generation-test-harness` asserts
`expect(child.kill).not.toHaveBeenCalled()` on `process.platform === 'win32'`,
which is the contract the previous commit deliberately replaced — and it
branches on the real platform, so it is dead code everywhere CI runs today.
The harness now asserts the handle-addressed root kill on every platform. On
win32 it lands after the tree walk, so the expectation waits rather than reading
one tick early, and its ten call sites await it. Red against the pre-fix arm at
all seven sites; the production code is unchanged.
* test(crash-reporting): remove the Windows lane marker tree through the retrying helper
The new win32 spec teardown used a raw rmSync, which the windows-lane-tree-removal
boundary ratchet rejects — and which is exactly the EPERM the ratchet exists to
prevent, since this spec's marker directory is written by processes it has just
force-killed.
* fix(crash-reporting): only refuse pid-addressed tree walks, disclose the handle-less codex site
The own-Chromium gate refused the POSIX process-group arm of
signalProcessTree as well, which was new macOS/Linux behaviour: a stale
getAppMetrics() entry plus pid reuse would orphan a group that main reaps
today. A POSIX group only holds what Orca put in it, so the refusal is now
scoped to win-taskkill-tree and the POSIX arm is recorded and admitted like
the other group kills in main. That also drops the synchronous
getAppMetrics() read from every POSIX termination.
codex-turn-added-roots kills roots found by a table walk, so a refusal has
no handle to fall back to. Pin that the refusal is visible - crumb written,
turn reported as not cancelled - rather than fixing what cannot be fixed.
* test(crash-reporting): detach the Windows survival fixture and observe real spawns
|
||
|
|
a65332a8bd |
feat(claude): move structured native chat onto the Claude Agent SDK and enable it on macOS and Linux (#18560)
* Join structured attach teardown through journal bind * fix: restore structured chat parity * feat: add Claude structured session adapter * fix: harden Claude structured adapter * fix: close Claude adapter edge cases * fix: start Claude init deadline after launch * feat: wire Claude structured sessions * fix: harden Claude structured runtime * fix: fence Claude structured compatibility * fix: preserve Claude free-text prompt answers * fix: decode addressed Claude prompt text * feat: enable Claude structured chat on mobile * fix(mobile): keep structured chat provider-aware * fix(mobile): negotiate Claude structured tabs * fix: keep scoped RPC tests native-free * fix: secure mobile structured image delivery * fix: close structured session data-loss gaps * fix: prove real Claude structured startup * fix: consume pre-spawn proof before retry * feat(native-chat): add desktop structured sessions * fix(native-chat): satisfy structured session cleanup gates * fix(native-chat): keep structured renders pure * fix(native-chat): open composer pickers upward * fix(native-chat): use existing view for structured sessions * fix: harden structured desktop status projection * fix: close structured desktop lifecycle gaps * fix: fence structured AI Vault resumes * fix: fence structured AI Vault resumes * fix: preserve structured tabs during activation * feat: toggle structured sessions between chat and TUI * fix: harden structured session handoffs * fix: bind structured TUI before rollout proof * fix: complete structured chat round trips * fix: align structured TUI return readiness * fix(native-chat): make reverse handoff transactional * Add Claude structured TUI handoff seams * fix(native-chat): clear sticky handoff recovery * fix(native-chat): complete mobile reverse after TUI exit * fix(native-chat): keep TUI transcripts readable * fix(native-chat): recover TUI transcript gaps * fix(native-chat): recover claimed TUI owners * fix(native-chat): retain cold TUI proof authority * fix(native-chat): preserve Claude handoff authority * fix(native-chat): recover TUI transcripts read-only * fix(native-chat): harden Claude handoff recovery * fix(native-chat): serialize structured handoff recovery * fix(native-chat): close handoff admission races * fix(native-chat): validate pinned launch environment * fix(native-chat): revalidate restored and retried owners * fix(native-chat): gate restart recovery publications * fix(i18n): catalog Claude session controls * fix(native-chat): wait for structured TUI process proof * fix(native-chat): queue stale idle TUI handoffs * fix(native-chat): route structured Codex options directly * fix(native-chat): persist structured session options * fix(native-chat): hydrate resumed structured options * fix(native-chat): preserve options across structured handoffs * fix(native-chat): replay pending option mutations * fix(native-chat): rotate settled handoff operations * fix(native-chat): rotate refused send operations * test(native-chat): derive refusal retry state from host * test(native-chat): give the host-oracle matrix test an explicit timeout * fix(native-chat): keep Claude option controls idle * fix mobile structured first-send hydration race * fix(native-chat): preserve handoff launch authority * fix(native-chat): harden shared handoff recovery * fix(native-chat): serialize structured handoff recovery * fix(native-chat): close handoff admission races * fix(native-chat): validate pinned launch environment * fix(native-chat): revalidate restored and retried owners * fix(native-chat): gate restart recovery publications * fix(i18n): catalog structured session recovery control * fix(native-chat): wait for structured TUI process proof * fix(native-chat): queue stale idle TUI handoffs * fix(native-chat): keep structured recovery provider-neutral * fix(native-chat): drop local terminal topology from structured sync * fix structured outbox and tab restore races * fix(native-chat): preserve Claude question groups * fix structured provider visibility and request handling * fix structured session TUI handoff recovery * fix reverse structured session handoff * fix(native-chat): recover Claude outbox and resume state * chore(mobile): preserve the working-tree lockfile state before the main merge Carries the pre-existing uncommitted mobile/pnpm-lock.yaml modification into history so the main merge cannot overwrite it. Verified benign pnpm drift (babel 7.29.7->7.29.8 transitives plus deprecation metadata); drops no patchedDependencies (the mobile lockfile declares none). * test(native-chat): drop orphaned Claude handoff-auth test left by the main merge 'pins Claude handoff auth through the terminal provider boundary' is absent from main and its production counterpart preserveClaudeAuthEnv no longer exists outside this test - orphaned residue of the terminal/native handoff work this PR excludes by scope. Removed rather than repaired: the failure was a renamed field (providerHome -> providerRoot), and renaming it would have carried out-of-scope handoff code into the merge. Body preserved as evidence and logged in CLAUDE-STRUCTURED-DISPOSITION-TABLE.md. * Fix mobile structured turn state * fix Claude structured session blockers * fix claude structured lane blockers * fix Claude acquisition exit proof * fix(claude): route stream-json launch through process wrapper * fix(claude): gate structured chat support * Fix Claude structured launch gating * fix(claude): split session acquisition and prune mobile scope * test(claude): align structured session fixtures * fix(agent-session): preserve handoff launch arguments * fix(claude): open journals through the factory after origin/main split The journal opener moved to journal-store-factory on main; retarget the Claude structured tests that still imported the old path. * fix(claude): resolve Claude structured launch args, auth, and win32 proof The origin/main merge re-expressed the lane's Claude wiring onto main's split orca-runtime facade and dropped three wires past green typecheck and lint. - resolveLaunchArgs discarded its provider parameter, so structured Claude sessions were launched with Codex app-server flags; Claude exits on --dangerously-bypass-approvals-and-sandbox, and a Codex arg-parse throw could block Claude session creation outright. - resolveClaudeLaunchEnv was no longer supplied, so the launch resolver fell back to the whole process env as configuredEnv and buildClaudeChildProcessEnv re-applied every auth var it had just stripped. The resolver now merges the Claude overlay onto a strip-applied copy of the inherited env, which also keeps PATH intact for withCliRuntimeOnPath. - The windowsProcessStartTimeAvailable producer was gone while the contract field and both consumers survived, so the renderer gate fail-closed and structured native chat was unreachable on every win32 host. Separately, structured Claude pinned CLAUDE_CONFIG_DIR unconditionally. An explicit pin makes the CLI abandon the macOS Keychain even when it names the CLI's own default, so a default claude.ai account could not authenticate where the legacy Claude terminal could. Pin only a home the CLI would not resolve on its own, matching ClaudeRuntimePathResolver, and compare against the env the child would otherwise inherit so a diverging overlay cannot outrank the record's account home. Also await the now-async revealNativeSession in its regression test, and set the native status before revealing so a rejecting reveal cannot leave a session released but never marked native. Claude-Session: https://claude.ai/code/session_013UqKCRB6k5e8UaYhXUHeWY * fix(claude): scrub case-insensitive Windows auth env * fix(native-chat): settle handoff outcome-write failures instead of leaking them A store write failure while recording a handoff outcome escaped the flow runner's catch handler, so the client never received the failure and the flow surfaced as an unhandled rejection (seen as an intermittent agent_session_store_corrupt error in the proven-dead-retry suite, whose teardown raced the flow's trailing outcome write). Record the failed outcome best-effort, and drain the coordinator before that test's teardown removes the store root. Claude-Session: https://claude.ai/code/session_011aXkcHyeiRJuezupQdjZaM * fix(native-chat): make the structured close-failure toast provider-neutral The structuredSessionCloseFailed toast fires for any structured session, but its copy said 'Codex chat', so a Claude structured session that fails to close showed the wrong provider name. The launch-failure toast is only reachable behind the agent === 'codex' gate, so its copy stays as is. Claude-Session: https://claude.ai/code/session_013ugSpCx4AWkySaJb69BQax * fix(native-chat): wire structured handoff proof recovery * fix(native-chat): wire structured handoff proof recovery * fix(native-chat): correct the structured chat opt-in copy The one `experimentalStructuredNativeChat` toggle gates both providers — `useStructuredAgentSessionCreate` runs `canUseStructuredNativeChat` for `'claude'` as well as `'codex'` — but its description named only Codex. Its scope line also said Windows keeps using terminal chat, while the gate refuses win32 only until the host proves it can read a process start time. `structured-native-chat-availability.test.ts` already pins that Windows is allowed once the proof is cached, so the two contradicted each other. Claude-Session: https://claude.ai/code/session_01RJFsidQWmKYFmeoUuVu4Tp * test(claude): pin @anthropic-ai/claude-agent-sdk 0.3.251 contracts against a scripted CLI PR 1 of the SDK migration: dependency + test-only harness, no product wiring. - Pin @anthropic-ai/claude-agent-sdk to exactly 0.3.251 — not the newest release — because 0.3.251 (published 2026-08-28) clears the repo's 3-day minimumReleaseAge supply-chain gate with no exclusion, while the newest release was minutes old and would have required excluding a brand-new publish from the exact control built to catch brand-new malicious publishes. Every contract this design depends on was verified identical on 0.3.251: the full option surface, no pid on SpawnedProcess (custom spawner stays mandatory), env defaulting to process.env when omitted, and --replay-user-messages appearing only via extraArgs. - Exclude all eight bundled CLI platform binaries via ignoredOptionalDependencies. The setting lives in pnpm-workspace.yaml because pnpm 12 no longer reads the package.json "pnpm" field (it warns and ignores it; verified by install ablation). Excluding the binaries is what makes Orca's pathToClaudeCodeExecutable override mandatory rather than merely preferred. Note: pnpm 12.0.0 honors the ignore list when reconciling an existing lockfile but not on fresh resolution of a new dependency, so the lockfile's SDK entry was pinned surgically; both 'pnpm install' and 'pnpm install --frozen-lockfile' verify clean and stable against the committed lockfile. - Contract-pin suite drives the real SDK against a scripted fake CLI and pins: unknown type/field/content-block pass-through (and keep_alive interception), spawner env fidelity plus the omitted-env process.env inheritance sharp edge, extraArgs producing --replay-user-messages, argument parity for every CLAUDE_STRUCTURED_BASE_ARGS entry plus --session-id/--resume/ --resume-session-at, canUseTool wire request_id stability and abort on control_cancel_request, one spawn per query, pathToClaudeCodeExecutable honored by the default spawner, the exact SDK version, and the eight platform binaries staying uninstalled. Claude-Session: https://claude.ai/code/session_01FGCRfYUnb4hbvfTAHGtJKQ * feat(claude): drive the structured transport through the agent SDK Replaces the hand-rolled `claude -p --input-format stream-json` transport with @anthropic-ai/claude-agent-sdk 0.3.251, keeping the existing connection interface for this commit so the acquisition path changes minimally. The control-plane rewrite is a separate change. Orca still supplies the process. `spawnClaudeCodeProcess` routes through `spawnProcess`, retains the child and its pid — the triple the durable lease adjudicates on — drains stderr so exit errors keep their tail, and hands `.cmd` shims to Orca's Windows argument encoder rather than the SDK's plain spawn. `close()` keeps Orca's own bounded tree-kill and exit deadline, so it still resolves true only after an observed exit. Launch resolution emits an SDK options object instead of argv; durable `launchArgs` translate to a typed option where one exists and to `extraArgs` otherwise, refusing a token neither can carry rather than dropping it. The child env is always passed explicitly — omitting it would let the SDK inherit `process.env` and reintroduce the ambient `ANTHROPIC_*` leak. The stdout line parser is deleted; the SDK owns framing, and unknown frames still reach the translator verbatim. Claude-Session: https://claude.ai/code/session_01JMhFjh9HEnkcJ5YTfCdgD3 * fix(claude): settle the frame the SDK pulled but never wrote The SDK's input pump is `for await (frame of prompt) { await transport.write(frame) }`. When that write rejects — the child dies between Orca's liveness guard and the write — the for-await ends abruptly and calls the generator's `return()`, so the code after `yield` never runs. The frame was already shift()ed out of `queued`, so the later `fail()` from the exit path could not reach it and `send()` never settled: `dispatchClaudeTurn` awaits that send before it can return `unknown`, wedging the caller and the durable outbox. The pre-SDK transport rejected on the stdin write callback instead. Retain the in-flight entry and settle it from the generator's cleanup, and let fail() reach it too for the pump that never resumes at all. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(claude): keep the agent SDK behind the structured-Claude boundary The ordinary OrcaRuntimeService graph statically reaches the Claude adapter and so the transport module, whose first line imported @anthropic-ai/claude-agent-sdk. The SDK is evaluated whenever the regular runtime loads, before any structured Claude session is chosen: it sets process.env.NoDefaultCurrentDirectoryInExePath, changing Windows executable resolution for later subprocesses, and a missing or incompatible install would break normal runtime startup — for a user who never leaves the terminal/TUI path. Defer the SDK to the connection, memoized so it loads once per process, and add the import-graph ratchet: a walk from the Electron main entry that fails on any static import of the package, plus a clean-fork check that loading the runtime leaves the Windows search variable untouched and a child-process pin that the side effect is still real. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(claude): answer list_models so the picker stops serving the seed sendControlRequest had no list_models case, so every request hit the default reject; readClaudeStructuredSessionOptions swallows that with .catch(() => null) and falls back to the static catalog. Every structured session therefore served a hardcoded model list with no per-model effort levels, no resolvedModel and no default detection, and nothing surfaced the failure. The pre-SDK transport got the live catalog from the CLI. Route it through the SDK's supportedModels(), wrapped in the { models } envelope the existing parser reads. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(claude): reap the child's descendants before killing it The forced step of the exit ladder went through the Codex helper, which spawns `pkill -KILL -P <pid>` and SIGKILLs the parent in the same tick: the parent usually dies first, the descendants reparent to pid 1, and `-P` matches nothing. An MCP or launcher descendant of a stubborn Claude child was left running. The test named for that requirement declined to assert it and killed the survivor by hand instead, so it could not fail for the thing it was named after. Route the Claude reap through Orca's existing sweep, which snapshots descendants while their parent link still exists and signals them before the root goes, and on Windows uses the identity-gated `taskkill /T /F`. The test now asserts the descendant is dead; the manual kill stays only as a failure-safe. close() still returns true only on an observed exit. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(native-chat): merge the duplicated handoff type import CI's static-analysis lint (`oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings`) exits 1 on the two separate `import type` statements from the same module. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(claude): answer a permission callback whose signal already aborted settleFrom registered the abort listener and then delivered the request. A callback that arrives already aborted never fires that event, so the promise stayed pending behind a durable prompt with no cancel path. Check the signal first, emit the cancel, and resolve the SDK's null sentinel without registering. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * test(claude): wait for the child to record the frame, not just for its report The scripted CLI writes its report at startup, so `until(readReport)` returned a report with no user messages whenever the child had not yet read the line. The assertion then failed under parallel load. Poll for the frame instead of for the file. Claude-Session: https://claude.ai/code/session_01AobxxokqQ3qcxS7sy7ckum * fix(claude): coalesce partial deltas onto one assistant item and stop painting result frames Under --include-partial-messages every stream_event frame carries its own uuid, and the final assistant frame for a block carries yet another; only message.id ties them. The translator keyed each delta by its frame uuid, so a reply painted as one bubble per delta chunk followed by a complete duplicate under the final frame's uuid. The block's first stream frame now mints the claude:(sessionId, uuid) identity, deltas coalesce onto it through the shared 60ms seam, and the final frame reconciles onto that same item. Known SDK bookkeeping no longer reaches the provider-fallback row: result subtypes are catalogued and settled by the turn lifecycle, an empty thinking block (redacted thinking) is a modeled kind, a string-content user replay is a text block, and an empty user frame paints nothing. An unmodeled result subtype or content kind still lands on the bounded fallback row. Claude-Session: https://claude.ai/code/session_01GaP5HpYQbvy2hYehVhwfEW * fix(claude): prove descendant exit at the close boundary instead of on an unref'd timer close() reported proven=true as soon as the direct child exited while the descendant sweep's SIGKILL sat on an unref'd 2 s timer, so a SIGTERM-resistant MCP server outlived the lease release. The reaper now composes the same shared primitives the Codex structured provider uses: snapshot, verified bounded descendant termination on POSIX, taskkill /T /F on Windows. The proof is false whenever descendants outlive the deadline, a retried close re-verifies the retained snapshot rather than trusting the dead root, and the raw pipe child no longer goes through the PTY job sweep it never owned a job for. Measured on macOS: a killed child of a SIGSTOPped parent stays a matching zombie row in ps, so the root is killed while verification runs rather than stopped first as the Codex non-group path does. Claude-Session: https://claude.ai/code/session_0161QFm3KVRNJKfdzWVGVNWk * feat(claude): replace the hand-rolled control plane with the SDK's native surface PR 3 of the Claude structured SDK migration removes the wire-frame scaffolding PR 2 kept, so Orca drives the SDK's typed control surface directly. Inbound permissions move from a rebuilt control_request dispatch to the SDK's canUseTool / onUserDialog callbacks. The prompt registry now carries the callback's own resolver: a decodable can_use_tool becomes a durable prompt whose answer settles the callback; a malformed one is denied without registering; the SDK's abort signal (fired on control_cancel_request, which the SDK matches and dedups itself) forgets the prompt and settles it null, and a late answer after abort finds no prompt and is refused. Closing settles every in-flight callback so no promise dangles. The claude-agent-sdk-control-bridge that rebuilt the wire frame is deleted. Outbound control maps to Query methods: interrupt() for cancel, setModel / setPermissionMode / applyFlagSettings for options, supportedModels for the model list, initializationResult() for init proof, each under Orca's own request deadline and error classification. Cancel is interrupt-receipt aware: a CLI advertising interrupt_cancel_queued_v1 gets cancel_queued in one round trip, otherwise the receipt's still_queued uuids are swept with cancel_async_message so a cancelled turn cannot spawn a later unexpected turn; older CLIs resolve no receipt. Init keeps the 10s deadline and the unauthenticated-startup guidance. Every behavior is failing-first and ablation-proven; the toggle-off import boundary and the accepted loss of unknown-control visibility rows are unchanged. Claude-Session: https://claude.ai/code/session_01Pqjduxt5G4rr9aYvtp7rNm * fix(claude): arm the descendant snapshot before stdin closes and make the tree verdict unproven by default A healthy Claude root leaves within the graceful window, and the close ladder only snapshotted descendants when the root was still alive after that window. So the common close never looked at the tree: `treeExited` stayed null, `!== false` passed it, and close() reported a proven exit with an MCP child still running. A root that died before the walk made the snapshot vacuous too. The proof is now unproven by default. The reaper holds one verdict in Orca's vocabulary (exited / live / unverifiable), assigned in exactly one place from the bounded verification, and close() returns true only on `exited`. The snapshot is armed before stdin closes, while the root can still be walked, and is verified after the root exits; a root that left before any snapshot could be armed stays unverifiable rather than vouching for descendants it never showed us. The shared verifier gains the three-way verdict behind its boolean face, and the connection reports the root and tree verdicts separately along with the child's exit status. One verification per close attempt: the retried close re-verifies, so the intra-attempt re-reap is gone from the teardown budget. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(claude): verify the Windows tree after taskkill instead of trusting that it ran `terminateWindowsProcessTree` resolves from taskkill's callback whatever the error says, so a timeout, an access denial, a recycled root and a surviving descendant all looked identical to the reaper — which then returned a proven exit unconditionally. close() reported true and the lease was released with an MCP descendant potentially still live. The Windows branch now snapshots the root's descendants while it is alive and, after taskkill, polls a fresh process table to a bounded deadline: a row still matching by pid AND creation time is `live`, an unreadable table is `unverifiable`, and only a table with no match is `exited`. Creation time is the PID-reuse guard the POSIX path gets from ps lstart, so a descendant that denied a creation-time query is omitted rather than signalled on a bare pid. A root already observed exited is never taskkilled: `/T /F` on a recycled pid would take an unrelated tree down with it. The captured tree is tagged by platform so neither verifier can be handed the other's rows. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(claude): release a reservation on a first-hand root exit instead of latching it into manual recovery Making close() strict about the descendant tree exposed a second defect at the same boundary. A create-time acquisition has no ownerProcess until publication, so an unproven cleanup mapped to handoffStage `manual-recovery`, and adjudication then refuses every later attach with agent_session_ownership_unknown. A user who was merely signed out, or whose --resume the CLI rejected, wedged the session id permanently. Each question now answers from its own evidence. close() is unchanged and stays strict about the tree. Separately, the lease is keyed on the root's pid and start time, so when Orca's own child handle observed that root exit and no descendant snapshot was ever admissible, the reservation is released and the CLI's exit code and stderr reach the user. A descendant observed still alive, or a root Orca never saw leave, stays unproven and keeps the reservation. The settlement records only what was observed: the released lease says the provider process exited and its descendants were not verifiable, rather than reusing the wording that claims cleanup proved no child remains. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(claude): surface an API error a result frame reports instead of settling the turn on it The SDK models an API failure as a SUCCESS-subtype result whose `result` string is the user-facing error text, with no assistant frame behind it. The translator suppressed every catalogued result subtype as turn bookkeeping, so that turn tombstoned its lifecycle and showed the user a completed, empty reply with no sign anything had failed. Suppression is now by meaning. A result reporting a failure routes to the bounded provider-error surface, leading with the provider's own sentence and keeping the raw frame behind the row's disclosure; ordinary successful results stay off the timeline as before. A turn the user aborted also stays suppressed: its interrupt frame already says so, and its execution diagnostic would only be noise on every stop. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(claude): drop the stream state of turns that never received their final frame Every streamed delta recorded its block's identity, latest text and checkpoint length. Only the final assistant frame removed them, so an interrupted turn left its whole accumulated reply reachable until the session was disposed, and a long session with repeated interruptions grew those maps without bound. The partial text was already journaled by the flush that precedes settlement, so the live copy was pure retention. That state now lives in its own module, named for what it does — grow a streamed block's journal row between its deltas and its final frame — and turn settlement drops every block still awaiting a final. The translator reports how many remain, which is the invariant: a settled turn leaves none. Also makes a timed-out process-table read retryable while the root is still alive. A loaded host can miss the table's one-second deadline, and latching that as "no descendants" both lost the descendant sweep and, on a busy machine, made the close ladder report unproven for a tree it never actually looked at. Only the root's death still makes a missing snapshot final. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * perf(claude): capture the Windows descendant tree from one process-table read The capture walked the descendant tree and then read the table again for the creation times the walk's projection drops. Each read is bounded in seconds and both run inside the close ladder's budget, so the second one cost the worst-case teardown three seconds for data the first read already held. The walk is now exported from the module that owns it and runs over rows the caller has already read, which is also what lets the snapshot keep the PID-reuse guard the projection cannot carry. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(pty): spend the descendant verification window instead of surrendering on one slow table read The verification abandoned the whole check the first time a process-table read missed its own one-second deadline, with seconds of its window still unspent. On a loaded host that reported a tree unverifiable without ever having looked at it, which the Claude close ladder then turned into an unproven close and a retried teardown. It also made the descendant-exit tests flake under a parallel suite run, for the same reason and with the same honest-but-premature verdict. A read that missed its deadline is now simply not an answer: the loop waits and reads again until its own deadline, and only a window that ends without a readable table reports unverifiable. This can only turn a premature verdict into one backed by evidence; it never manufactures a proof. Claude-Session: https://claude.ai/code/session_01BSmXgkWSsNHft8jFkdBFG9 * fix(claude): never let a later failed look collapse an observed live descendant into unverifiable The reaper's single assignment site latched only 'exited', so a second reap whose table reads all missed their deadline overwrote an earlier completed verification's 'live' with 'unverifiable'. The acquisition release gate discriminates on exactly that pair, so a root exit after such a decay released the lease over a descendant that had been observed alive. The latch is now monotone in trust order: exited is final, and live is only ever raised to exited. Claude-Session: https://claude.ai/code/session_01HfdhsvSJucLw4cTZxzg2CP * fix(claude): never prove a Windows tree gone while a descendant denied identification The Windows snapshot dropped rows that denied the creation-time query, and an emptied snapshot was judged exited without any table read: a descendant Orca was refused information about was treated as one that had left. The snapshot now counts the unidentified rows it saw, and verification caps its verdict at unverifiable while any exist. Nothing is ever signalled on a bare pid, as before. Claude-Session: https://claude.ai/code/session_01HfdhsvSJucLw4cTZxzg2CP * fix(claude): classify cleanup after a first-hand exit as a root exit instead of a proven tree When the CLI died between a successful acquire and the host's commit or proof of the lease, handleExit had already removed the session, so releaseAcquisition found nothing and reported true. The attach flow then settled exit-proven with deathEvidence claiming cleanup proved no provider child remains, though the tree was never verified. The adapter now keeps the exit that removed a published session until the session is acquired again; acquisition cleanup runs that connection's close ladder and classifies its verdict exactly as a start-time failure would be, so the record reads root-exit-observed. The wire helper keeps that typed classification and its provider diagnostic instead of wrapping it as unproven, and the router gives up its owner even when the release throws. Claude-Session: https://claude.ai/code/session_01HfdhsvSJucLw4cTZxzg2CP * fix(claude): integrate SDK teardown and picker lifecycle fixes * fix(claude): preserve resume leaf and settle processless spawns * fix(claude): reacquire from persisted resume leaf * fix(native-chat): restore Claude grouped question handling * fix(claude): persist only resumable transcript leaves * fix(claude): recover structured session exits safely * fix(claude): close remaining structured session P1s * fix(claude): harden transcript branch proof * Remove superseded root fix reports * fix(windows): restore indexed descendant row walk * fix(router): forward force-close lifecycle * fix(claude): fence stale turn cancellations * fix(claude): fence cancellation after unknown dispatch * fix(claude): fence replay and option recovery races * fix(claude): block replay fallback after waiter eviction * fix(claude): fence evicted slash results * fix(claude): fence ambiguous results and restore options safely * fix(claude): scrub SDK child env and localize pending launch * fix(claude): pin transcript roots and exit recovery proofs * fix(claude): retain unproven SDK exits * fix(claude): settle retained exit before reacquire * fix(claude): resume from settled retained cursor * chore: remove tracked review artifact * fix: harden Claude SDK transport session cleanup * fix: close Claude sessions safely * fix(claude): close races with fresh child snapshots * fix(claude): fail closed on recycled child identities * fix(claude): gate root cleanup on process identity * fix(claude): fence same-second root identity reuse * fix(claude): restore the root SIGKILL fallback the identity gate took away The direct root kill goes through the handle Node owns, not through a pid: libuv drops that handle in the same turn it reaps, so the signal either reaches the process Orca spawned or reaches nothing at all. Gating it on a process-table probe therefore bought no safety and cost the tree its only fallback whenever the probe declined -- a first capture landing in the fork's own second, a recycled descendant pid voiding the snapshot, or a process table that could not be read on either platform. Identity verification stays where a bare pid is genuinely addressed: Windows `taskkill /T /F`, and the descendant sweep's own revalidation before it signals. Also stops a declined root probe from collapsing an observed `live` or `exited` descendant verdict into `unverifiable`, and stops a successful taskkill from reporting `unverifiable` because a later probe found the root correctly dead. * docs(claude): rewrap the root-kill ordering comment * Match the Claude structured launch to the terminal path's managed-account auth rules The SDK path stripped ambient Anthropic auth unconditionally, let an explicit agentDefaultEnv override beat a pinned managed account, and had no account-switch guard. Reuse the terminal preflight's own predicate and messages so both transports strip, refuse, and report identically, and cover the CLI transcript location that mobile native chat depends on. * Reach the Claude structured chat lane from the desktop UI The main process has had a complete, correctly gated Claude Agent SDK lane for a while, but no renderer ever asked for it: the launch route accepted only `codex`, and the create path was typed `agent: 'codex'` end to end. Widen both to the structured provider union that already exists (`AgentSessionHandleProvider`), and generalize the codex-named create path instead of adding a Claude twin beside it. The pending-launch registry is now keyed by agent as well as workspace — a shared key handed a second caller the first agent's intent, so a Claude and a Codex launch in one worktree collided. Windows, per agent. Codex's client-side win32 refusal is deliberate and settled elsewhere, so it stays exactly as it was. Claude's answer is no longer guessed from the client's platform: a structured session fences its provider child on that child's process start time, and only the executing host knows whether it can read one. `agentSession.createSupport` already answers precisely that, per agent, and had no renderer caller — so the Claude create path asks it before creating and turns a "no", or a probe it cannot get answered, into the definitive refusal the launch fallback already handles. Fail closed either way. That refusal mapping also closes a real gap: the host reports an unsupported location by throwing `structured_agent_session_unsupported`, which reaches the client as a transport rejection rather than a refusal envelope, so `StructuredAgentSessionCreateRefusalError` never fired. The launch would retry the create, strand itself in `visibilityUnknown`, run no legacy fallback, and show an error toast. Close a fail-open hole while Claude and win32 become reachable: `create` with a client-supplied location, and `ensure`, both skip the worktree-resolving support check. They now ask the executing host the same question directly, so a host that cannot fence a provider child no longer creates one on a client's say-so. Also deletes `structured-agent-session-provider-routing.ts`, a duplicate of `structured-agent-session-provider-support.ts` with no importers. WSL, SSH and paired hosts, floating workspaces, draft prompt delivery, explicit TUI customization and initial session options all keep refusing; folder workspaces keep working. * P1-1: make the structured Claude auth policy required and testable The optional dep plus a {stripAuthEnv:false} fallback meant a dropped wiring under-stripped silently. Required at all three hops, asserted at install time for the @ts-nocheck caller, and the settings-to-policy mapping is now a named tested function. * P2-3: mobile's default Claude transcript root must follow CLAUDE_CONFIG_DIR session-file-resolver's default ignored the variable the pinned account home follows, so a CLAUDE_CONFIG_DIR launch wrote one tree and mobile read another. The Task-4 test now resolves with no root override (mobile's own call) and checks the answer against the root the CLI itself reports, instead of mirroring the code under test's own expression. * P2-1/P2-2/P3: close the teardown window, join the live-auth gate, align the refusal P2-1: a switch beginning inside the acquire teardown left a dead chat and no replacement. Past that point the launch waits the swap out and refuses only if it never settles; the entry guard still refuses outright, because nothing is torn down there yet. P2-2: structured children now hold the same OAuth-refresh gate a Claude PTY does, so a managed refresh cannot rotate the token out from under a live turn. P3: the refusal now matches the strip it guards (case-folded on win32, presence not truthiness), and the dead structured-to-TUI builder states its auth policy instead of silently signing a system-auth user out. * Make the live-auth gate tests independent of sibling connection teardown order * Do not offer structured Claude under a WSL-only managed account Structured Claude launches against the ambient Claude config, which the account service keeps in sync with the selected HOST account. A WSL-bound managed account lives inside the distro and is never synced there, so on Windows a structured session would authenticate as whatever the ambient identity happens to be while the UI names the WSL account — the user is told one identity and given another. That was unreachable only because nothing offered structured Claude on win32. Enabling it makes it reachable, so gate it here rather than patching the auth layer: refuse the structured path when the active managed Claude account is WSL-bound, and let the terminal-backed path — which resolves the account per runtime — handle that account shape. The answer rides the agentSession.createSupport seam the renderer already consumes, so no new capability and no renderer knowledge of account internals. A create the host declines becomes the definitive refusal the launch fallback already turns into a legacy native chat tab, with no error toast. Unknown answers refuse. An install with no managed accounts claims no identity and is fine, but an active selection that cannot be resolved — or account state that cannot be read at all — is not evidence that the ambient identity is right. Claude only. Codex resolves its account through a different path and its createSupport answer is untouched, as is every Codex routing decision. * Read the structured Claude account gate through the auth policy's accessor The gate resolved the active account from the account-service snapshot's runtime map; the auth policy resolves it with getSelectedClaudeAccountIdForTarget(settings, { runtime: 'host' }). Those are two sources and two resolution rules, and they disagree on a legacy settings blob that carries the selection only in the flat activeClaudeManagedAccountId: the accessor falls through to it, a direct read of the runtime map does not. The gate would then refuse a launch the policy would have run under host-1 — and in the mirror case a session could be admitted under a policy computed from a different account than the gate approved. Read the same settings through the same accessor so agreement is structural rather than coincidental, and drop the controller accessor that existed only to reach the snapshot. No behaviour change for any state both already agreed on; Codex is untouched. * Round-3 review fixes: N-1 empty-value regression, N-2 gate leak window, N-4 lost history N-1: my presence-based conflict predicate refused a terminal launch that works today. 'ANTHROPIC_API_KEY=' is how a user blanks a variable and the settings pipeline preserves that empty value; an empty override cannot beat the pinned account and the strip removes the name anyway. Back to truthiness for the value, keeping the win32 case folding. N-2: enter the live-auth gate only after the exit/close handlers that release it, so no throw in between can leave an entry nothing reconciles. N-4: the Claude transcript resolver searches config-dir-then-default and de-dupes, matching the Codex sibling in the same file, so adopting CLAUDE_CONFIG_DIR no longer hides history written before it. * Run the managed-account gate on every Claude acquisition, not just create createSupport gates the create path, but a session's account state can change while it lives. A reacquire after an unexpected child exit re-resolves the launch and re-derives auth, with nothing re-checking the gate — so a session created while supported could come back up in the refused shape. With the strip predicate keyed on there being an active non-WSL account, the WSL-only user's normalized steady state (accounts exist, none active) does not strip, and that reacquire reaches the child with ambient auth while the UI names the account. Gate at resolveLaunch, the one choke point every acquisition passes through, refusing with the pre-spawn error the caller already handles. Same predicate as create-time, now sharing one settings reader so the two cannot drift. Claude only; Codex resolves its account on a different path and is untouched. The runtime class that wires this does not typecheck its own `this` calls — a missing hookup compiles clean — so the wiring is pinned behaviourally rather than trusted to the compiler. * Move the structured Claude gate out of the @ts-nocheck runtime files Both call sites of the managed-account gate sat in files whose first line is `// @ts-nocheck`, so neither was typechecked: three arguments to a one-argument function plus an undeclared identifier compiled clean. New auth-identity decision logic had no compiler behind it. Move the verdict into a checked module that takes the two facts the runtime owns — the adapter's answer and a settings getter — and decides. The runtime class now only forwards. Move the gate reader's construction into the checked installer too, so the nocheck file passes a plain settings closure and never names a gate symbol. Every reference to the gate predicate and its reader now lives in a checked file, so the ablation that used to pass silently is a compile error at both the create-support and reacquire sites. Removing the file-level @ts-nocheck is a separate, larger job and is not attempted here. * Derive the gate test's auth policy from the settings under test A hardcoded stripAuthEnv asserts a gate/policy pairing production cannot produce, and false additionally lets launch.env inherit the runner's real process.env. Derive via claudeStructuredAuthPolicyForSettings instead: the gate settings type is the same Pick the policy takes, and both resolve the account through getSelectedClaudeAccountIdForTarget. * Pin the absent-vs-empty distinction in the managed-account gate An empty claudeManagedAccounts array is a real answer: the user has no managed accounts, nothing claims an identity, and the ambient path is legitimate. A readable settings object with no such field is settings we failed to parse — the same unknown as unreadable — so it refuses. The two are one character apart in the code and the difference is invisible without the reasoning, so record it at the branch and pin both sides. The test fails under the obvious "consistency fix" of treating a missing field as empty. * fix(claude): keep command queue bookkeeping out of the transcript Claude Code 2.1.258 emits a `command_lifecycle` frame for every uuid-stamped command it starts, completes or cancels. The frame carries a command uuid and a state and no content, and the CLI keeps it out of its own transcript -- but it is absent from the SDK's SDKMessage union and so from Orca's frame catalogue, where an uncatalogued kind defaults to a substantive row. Every structured turn therefore painted raw JSON rows into the user-visible transcript. Catalogue it and disposition it as status chrome. The unknown-kind default stays `timeline-substantive`: a kind we have never seen is likelier to carry content than to be chrome, and a visible row we can catalogue later beats content we silently dropped. A lifecycle state that reads as a failure still surfaces, because the payload error check in `classifyProviderFrame` outranks the catalogue. * fix(claude): let a re-walked descendant become eligible for the forced sweep A descendant first observed by a capture inside its own birth second could never be SIGKILLed: `ps lstart` is second-resolution, so that capture cannot rule out a pid recycled later in the same second, and the merge pinned each retained row to the boundary of the walk that first saw it. SIGTERM-resistant children forked in that window were signalled and then never escalated -- they survived close, quit and restart, reparented to init, and had to be killed by hand. Advancing that boundary on any later capture would be unsound: a later capture matching pid, pgid and start-second is exactly what an impostor would also show. But a capture is not a match -- it is a fresh ppid walk from a root Node pins through its own handle, so a row it re-derives is proved ours at that instant without appealing to its start time. Chain the fence from there instead, and take that walk at the close boundary while the root certainly still lives: the root may leave inside the grace window, and the post-timeout refresh never runs. A row absent from the later walk still keeps its earlier boundary, and a row no walk has ever re-derived in a later second is still never escalated. * Treat an absent managed-account list as empty, not as unreadable An empty claudeManagedAccounts array and a missing one are the same answer: this user has no managed Claude accounts, so nothing claims an identity and ambient auth is the truth. Refusing on absence strands any profile that simply never wrote the key, and it disagrees with the auth policy, whose own predicate takes `(accounts ?? [])` for exactly this reason. Only settings that cannot be READ stay unknown, and those still refuse — as do a WSL-bound active account and a selection naming an account the list does not explain. The earlier reasoning treated a missing field as settings we failed to parse. That conflated "not present" with "not readable"; only the second is unknown. * Support structured Claude when accounts are registered but none is selected Registered-but-deselected Claude accounts were refused, which is behaviourally identical to having no accounts at all: the auth policy does not strip, ambient auth is the truth, and the UI names no host identity. A user who deselected their accounts silently got legacy chat with nothing explaining why. Nothing selected for the host runtime is two states the settings cannot tell apart after the fact, because pruneInvalidClaudeRuntimeSelection empties the host slot and persists null in the second one: honest deselection -> ambient auth, UI names nothing -> SUPPORTED the WSL-only steady state -> ambient auth, UI names the WSL account -> REFUSED The presence of any WSL-bound account in the list decides. Simplifying this to "none active -> supported" re-opens the auth-identity misrepresentation, so the tests fail loudly on exactly that: five of them, across the unit rule and the createSupport path. * Stop treating an unanswerable create-support probe as a refusal A worktree is not resolvable for a beat after createWorktree resolves, so a probe fired immediately after creation fails the RPC with selector_not_found instead of answering. The catch collapsed that into `supported = false`, so the composer refused and quietly built a terminal session — the gate never said no, it was never asked successfully. Elapsed time was the only input that decided whether a Claude launch went structured. "Could not answer" and "answered no" are different states and only the second is a verdict. Retry while the host cannot yet resolve the selector, with a bounded backoff that covers the measured window with margin, and keep refusing on the first ask for everything else. Fail-closed is unchanged: a probe that still cannot be answered when the budget is spent refuses. The retry is narrowed with the shared error-code matcher, which classifies a token that transports re-wrap into a longer message without matching prose that merely mentions it. Codex never probes, so this race has never been able to refuse a Codex launch — the race itself is identical for it. Recorded at the early return, because whoever gives Codex a probe inherits the bug. * fix(claude): fence the forced sweep on re-derivation, not on lstart's second A descendant forked in the same wall-clock second as every walk that sees it was signalled with SIGTERM and then never escalated, so a SIGTERM-resistant child survived tab close, app quit and a full relaunch. Two children of one parent 96ms apart across a second boundary took opposite paths. The leak predates this branch: it reproduces with the change reverted. `ps lstart` has one-second resolution, so a walk landing inside a row's birth second can never rule out a pid recycled later in that same second. But a walk is not a match: a ppid walk only reaches what the root actually parents, and the root is pinned by Node's own handle, so a row the walk re-derived is ours whatever second it was born in -- a stranger would have to have been forked into our tree, and then it is not a stranger. Fence the escalation on that. Rows a merge retained from an earlier walk are not re-derived and still answer to the start-time fence, which remains correct for them. Scoped to callers that revalidate identity before signalling, which is the Claude close path. Codex teardown reaches this same verifier and is unchanged; the argument holds there too, but widening it is its own deliberate change. Also reverts two changes from the previous attempt at this leak. Advancing the capture boundary on a later walk is inert once the sweep fences on re-derivation -- both key on the same set of rows, so the new term short-circuits for exactly the rows whose boundary it advanced. The extra ladder refresh was a duplicate full process-table read: close() already awaits tree.refresh() immediately before proveClaudeChildExit, on the only path that reaches it. Known property: the kill lands roughly a grace window after the walk that proved membership, so a pid recycled inside that gap could in principle be signalled. It is bounded -- matchingSnapshotRows already requires the live row to carry the same start-second and pgid, so an impostor must be born in the remainder of that one second, land on that exact pid, and sit in the same process group, and it has already received the unfenced SIGTERM from the same loop. * Run the Claude structured integration suite as a runtime client The suite exercises agentSession.* for Claude, not the mobile surface: nothing in it asserts anything mobile-specific and its sibling integration suites use 'runtime'. Mobile now additionally requires the experimental structured-chat setting, which structured-agent-session.test.ts pins in both states, so the stale 'mobile' fixture was claiming coverage it never had. * fix(claude): report effort from get_settings, which is the only frame that has it The composer's Effort pill rendered blank in every structured session. This is not a missing source: the publication reads `effortLevel` off the `system/init` frame, and that frame has never carried an effort of any kind, while the correct value is already fetched at acquisition and thrown away on the auth diagnostic. Verified two ways -- a live get_settings probe against Claude Code 2.1.258, and the shipped binary's own init frame construction, which lists `model` and no effort. So `reportedOptions.effort` was always empty, the options reader dropped the key, and the pill had no value. Model survived only because `currentModelId()` has a fallback chain. The get_settings call acquisition already makes reports the session's current effort as `effective.effortLevel`; pass that into the publication instead. Selecting an effort already worked, so this is the arrival value only. The legacy PTY path is unaffected and must not be "fixed" to match: it reads its effort by parsing the startup banner (`CLAUDE_MODEL_EFFORT` in src/renderer/src/components/native-chat/claude-terminal-session-options.ts), which is why it shows a value where the structured path does not. Also removes the fixture that hid this: the fake init frame invented `effortLevel: 'high'`, a field the CLI does not send, which is why every gate stayed green over a value that is always empty in production. The fixture's get_settings now returns the real {applied, effective, sources} shape instead of a bare `{env: {}}`, so the two adapter tests that asserted an effort keep asserting it through the path production actually uses. The reader returns null rather than defaulting: an effort nothing measured would repeat the fixture's mistake, and a blank pill is the honest degradation if the provider ever renames the key. * fix(claude): only record an effort the child confirms it adopted apply_flag_settings answers `success` for an effort it then ignores. Measured against Claude Code 2.1.258: applying `bogus-effort-xyz` returns subtype "success" with no error while `applied.effort` stays at its previous value, and a valid `low` moves it. The option write treated the absence of a throw as adoption and recorded the requested value unconditionally, so Orca would show and persist an effort the child was not using, with nothing anywhere reporting a problem. Read the effort back after applying it, through the same reader the arrival value uses, and reject when the child reports a different one. A readback that could not be taken is not evidence of a refusal -- the apply itself succeeded -- so it still records; only a readback that disagrees rejects. Not reachable from today's picker, which offers catalog values only, but the CLI's effort catalog is server-delivered and has changed before, so a retired id would otherwise become a pill confidently displaying a setting that never took. * test(claude): assert the effort contract against the real binary The blank pill survived every gate because the only tests that touched it were fixture-backed, and the fixture invented the field. A test that pins the shape we read cannot catch the provider renaming the key, which is the failure mode that produced this defect. Asserts both halves against a live authenticated CLI: that no frame it publishes carries an effort at all, and that the session's current effort arrives through get_settings. Which frame proves the session varies by host -- this machine proves it with a SessionStart hook rather than a system/init frame -- so the negative half asserts over every published frame rather than picking one. Skips with the rest of the file when no authenticated CLI is present. * fix(claude): stop the synthesised content-part kinds leaking into the transcript Sending an image put a bare `claude · message:user:content:image` row between the user's bubble and the answer. Two causes, and only the second is a family. An image part counted as modelled only when `source.type === 'url'`, but claudeDispatchMessageContent sends a local attachment as a base64 source and the CLI replays that shape back, so every attached image was classified unmodelled. Accept the base64 and file sources Orca itself sends. The family is the real defect. `message:<role>:content:<type>` kinds are synthesised at runtime from whatever `part.type` arrives, so unlike the top-level frame catalogue they can never be enumerated ahead of time -- the `?? 'timeline-substantive'` default then prints the synthesised name at a user who cannot act on it. That default is right for top-level frames, where "substantive" means show the frame; here it meant show our own vocabulary, which drops the content AND leaks the opcode. So an unrenderable part now renders a sentence saying exactly that, with the kind and payload still on the row's disclosure. A part that carries its own readable sentence keeps it -- the placeholder is a fallback, not an override. An unknown future part type is therefore visible, never silently dropped and never printed as a kind: the same principle as the effort readback, which records only what the provider confirms. * Declare agentSession.requestHandoff on the cross-version wire surface The manifest is a ratchet for cross-version reachability, so the method is declared with real HandoffParams rather than counted. requestHandoff is capability-gated through requireStructuredHost and has no client caller, so declaring it is the whole of the change. Also model two host capabilities the harness omitted: the stub host's supportsCreate, and the fake adapter's, without which adapterSupportsCreate falls through to a supportsLocation the fake also lacks. Every ensure was refused for the harness's silence rather than for its location. * Gate structured Claude session tabs on the client capability that names them The Claude structured lane deleted the projection's `agent !== 'codex'` filter and added CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY in the same commit, but never wired the constant to anything. Paired clients then received agent-session tabs for Claude, which no shipped client renders -- mobile's resolveMobileNativeChat returns null for every agent but codex, so the row listed and selected into a pane with neither chat nor terminal. Restore the filter behind the declared capability instead of the bare agent name. No client advertises it yet, so this matches main's behaviour today and becomes a negotiation a future client can opt into. * Confirm the structured Claude model against the model the CLI reports set_model answers success for any string, including a model it cannot resolve — the failure only surfaces when the turn runs — and get_settings reports the settings-file model, not the session's. The init frame that opens each turn is the only channel carrying the adopted model, so keep the session's reported model current from it instead of reading it once at acquisition. Also stop rejecting an effort the readback cannot represent: max is session-scoped and excluded from the persisted effortLevel, so a readback reporting the level underneath it is an absence of evidence, not a refusal. * Clear the session-option hedge when the provider confirms the value The pill claimed every option was unconfirmed for the life of the session: the renderer recorded each write as dispatched and nothing ever moved it, so a model the CLI had already reported back still read as unconfirmed. Carry the provider's own confirmation to the surface. Main reports which option ids the provider named rather than merely accepted, and the client re-reads options as a turn changes, because the frame that opens a turn is where the adopted model arrives. A value the provider has not reported stays hedged, including an effort whose readback could not be taken. The confirmed list is optional on the wire: a host that predates it sends nothing and the client keeps hedging, which is the behaviour it had. * Keep the model report current across an acquisition fence bump * Show the picked session-option value and let the provider report correct it The pill showed a "not confirmed" second tooltip line for any value we had sent but not yet seen reported back. Nothing acts on it, and for the PTY lane it was permanent — that transport has no report channel. The pill now shows the picked value immediately and the provider's per-turn report corrects it when the two disagree; a newer local write still outranks a report that precedes it. `dispatched` stays as a provenance member rather than collapsing into `applied`: it is produced independently by the PTY lane, and it is where the `confirmed` wire field lands, which would otherwise be unobservable. Effort keeps its readback and its rejection path. That matters more now, not less: with the hedge gone the rejection is the only user-visible failure signal on this surface, so a spurious one would be the loudest bug here. Skipping the readback for an effort the settings response structurally cannot echo is what prevents it — the response carries the persisted level, so reading it back for a session-scoped value would report the level underneath and fail a valid write. * Hedge a session-option value only when the terminal transport sent it Both lanes emit `dispatched`, so it could never say which one produced a value. The descriptor now carries the transport that built it, set once in the shared snapshot builder from a parameter that is required rather than defaulted — the builder is the only place a descriptor is constructed, so a new producer has to name its lane or fail to compile. The structured lane confirms every value from the provider's own per-turn report, which makes the hedge transient noise there. The terminal lane can only learn an outcome by parsing the screen back, and only for Claude: every other agent's `dispatched` value stays unconfirmed for the life of the session, so the line is the only signal that we sent something we never saw land. * Refuse an effort the session's model advertises no control for * Refuse tab mutations on a Claude row the client never negotiated The branch added a case asserting a client advertising only agent-session.structured.v1 may mutate a claude row. That is the same ungated behaviour the projection gate removes, encoded a second time — mutation authorization reads the projection, so hiding the row refuses the write. Assert that contract instead, and add the positive case for a client that does negotiate Claude rows. * Resolve the Claude session's current model in one place so the effort guard and the pill agree * Record an effort the child did not adopt instead of refusing the write apply_flag_settings answers success for an effort it then ignores, so the readback exists to detect that. Refusing on it made the detection a veto, and a veto is only correct if the readback can never be wrong about which model is current -- which it was, twice. The pre-flight guard already refuses a level the model advertises no control for, so the veto guarded a door that is now locked upstream. Keep the detection, drop the refusal: a disagreement records the child's own answer and omits the option from confirmed, so main stops vouching for a value the provider rejected without blocking the user's write. * Stop a slow whole-machine ps from being read as an absent process `ps -axo ...command=` pays a per-pid argv read: measured 1.15s for 1,948 processes (0.03s without `command=`), and CPU contention stretched the same capture to 6.0s. Two budgets sized for a cheap look then misreport a readable machine. The reader's 3s ceiling killed 6 of 20 consecutive captures at load 27, so every consumer answered "unverifiable" about a table it could read. Raise it to 15s, and stamp the capture instant at ps START so `capturedAgeMs` is the upper bound its contract promises -- a 6s capture used to report itself as freshly taken, understating staleness against a 5s kill gate. The TTL keys on completion so a slow capture still coalesces instead of forking ps per caller. `readStructuredTuiProcessIdentity` then spent its whole 5s wait inside one capture and concluded "no exact child" after a single look taken before the child existed (observed landing at ~3.5s). Absence needs a look that did not race the spawn, so require two captures before the deadline can end the loop. Both surfaced by the real-binary Claude TUI resume test, which failed ~1 in 5 under load; 14/14 now, 8 of those runs containing a capture the old 3s budget would have killed. * Let the desktop renderer negotiate Claude structured tabs The paired-client gate hides agent-session rows an agent the client cannot render. The desktop renderer's own IPC dispatches as clientKind 'runtime' advertising only agent-session.structured.v1, so the gate hid Claude rows from the surface this feature ships on. It renders them; it should say so. * Stop a slow process table from silently blinding every freshness gate Stamping `capturedAgeMs` at ps START made the number honest, and honest broke both consumers that read it. `ps -axo ...command=` measured 2.5-9.0s on an idle 2,002-process laptop and 4.0-18.6s at load 46, so the age it now reports lands past every budget: `planRelayPtySweep` refuses the stop as "too old", and the renderer's `admitRemoteForegroundEvidence` refuses the record outright. That second one is the expensive half and was outside the diff -- a refusal bumps `consecutiveInspectionErrors`, the poll scheduler backs off to its 10s floor, and agent-completion detection stops for the pane. The subsystem went blind on exactly the loaded hosts the honest stamp was meant to serve. The evidence-publishing read now gives up at 1,200ms instead of waiting out `PS_TIMEOUT_MS`. It is one budget for one question: these consumers ask whether an observation describes NOW, and past this it does not -- a late answer is refused by the age gate anyway, having first blocked a polled path for the whole capture, so a prompt `unverifiable` is both the truthful verdict and the cheap one. Both relay call sites already produce it from a rejection, and an admitted `unverifiable` costs a poll where a refusal costs the cadence. Identity proof keeps the full 15s through `getFreshProcessTableSnapshot`, because it asks whether a process EXISTS and must never read slow as absent. The budget bounds the wait, never the capture: the reader coalesces, so an abandoned wait leaves its capture running to fill the cache rather than forking a second whole-machine `ps` on the host that can least afford one. 1,200ms is bracketed rather than picked. The floor is the capture's own cost -- `command=` measured 1.15s for 1,948 processes on an idle host, and a budget under that answers `unverifiable` about a machine nobody is straining. The ceiling is the consumer's: 2,000ms, less the 500ms a TTL-shared capture may already have aged, leaves 1,500ms, and transit takes the rest. That ceiling only fits once the capture stops being charged twice. `ps` runs inside the RPC round trip, so its duration is already in `receiveDelay`, and `capturedAgeMs` is that same duration on the host's clock; summing them halved the budget this gate grants a host from ~2.0s of `ps` to ~1.0s, which is why a 1.2s capture arriving at 1.3s read as 2.5s old and was refused. Admission now takes the larger of the two. The sweep's gate keeps its sum, which is correct there: `evidenceAgeSinceListingMs` is stamped after the listing ARRIVES, so it measures planning time and overlaps nothing. A stated limit rather than an assumed one: 15s is not proven sufficient for identity proof. The same capture reached 18.6s at load 46, so that path can still time out and answer "no exact child" about a host it simply could not read in time. Narrowing it needs a cheaper question than a whole-machine argv read, not a larger number. The one test guarding this field could not fail. `beginPtyHandlerTest` installs fake timers, so `Date.now()` is frozen, the real reader reports exactly +0, and `0 <= 500` held identically for a hardcoded zero, for completion-stamping and for start-stamping -- while the real reader on that host returns thousands of ms. It now drives a measured age in and asserts the handler publishes it rather than restamping; that the reader MEASURES it correctly stays pinned separately, against a controllable clock. Both consumers get boundary coverage either side, and each new gate was ablated red before it went green. * Keep the compatibility fields off the capture the budget just abandoned inspectProcess falls back to processHasChildren and listProcesses to getForegroundProcessName, and both read the same TTL-shared capture with no budget of their own. On a slow host they joined the in-flight capture the budgeted evidence read had just given up on, so the call still blocked for the full 6-18s and the budget bought nothing -- once for inspectProcess and once per managed PTY for listProcesses. Use the degraded answers those helpers already give for an unreadable table, reached promptly. pty.hasChildProcesses keeps its unbudgeted fresh probe: it is a one-shot destructive gate that can afford to wait. --------- Co-authored-by: Merge Sim <merge-sim@local> Co-authored-by: Merge Sim <sim@local> |
||
|
|
3e4fd4a7af |
Shorten orchestration skill description under the Agent Skills 1024-char limit (#18683)
* Shorten orchestration skill description under the Agent Skills 1024-char limit The folded description was 1038 chars, so spec-conforming installers such as SkillStar rejected the bundled orchestration skill. Drop the two clauses already covered elsewhere in the same description: "decomposing work across agents" (implied by "structured multi-agent coordination") and "automation of the browser embedded inside Orca" (restated by the locked `orca-cli` embedded-pages sentence). Every routing trigger asserted by orchestration-skill-guidance.test.mjs, the orca-cli handoff boundary, and the Computer Use boundary are unchanged. Result: 958 chars. Add config/scripts/skill-description-length.test.mjs, which parses every skills/*/SKILL.md frontmatter with `yaml` and fails on an empty or >1024 char description, so the regression cannot return. orca-cli sits at 1015 and is left as is. Fixes #17935 * Keep the embedded browser in the orchestration description's orca-cli routing Restores the word "browser" in the orca-cli sentence ("and the Orca embedded browser") so agents scanning for it still route embedded-browser control to orca-cli. Description is 985 chars, 39 under the spec limit. |
||
|
|
f7e3af254a |
fix(pty): close the pseudoconsole and dispose the conout worker on Windows self-exit (F24) (#18635)
* fix(pty): close the pseudoconsole when a Windows shell exits by itself
`ClosePseudoConsole` is the only thing that reaps a ConPTY's console host.
node-pty calls it from one place, `PtyKill`, which starts by looking the baton
up by id -- and the exit watcher in `SetupExitCallback` erased that baton the
moment the shell died. So on the self-exit path (typing `exit`, how panes
usually close) the lookup missed, `PtyKill` did nothing at all, and the
pseudoconsole was never closed.
The baton now survives until BOTH the shell has exited and `kill()` has run;
whichever arrives second frees it. `PtyKill` copies `hpc` out under the lock and
closes it afterwards, guards `TerminateProcess` on a shell handle the watcher
may already have closed, and duplicates that handle rather than reordering, so
upstream's close-then-terminate sequence is unchanged.
Measured on Windows 11, 20 self-exit cycles driven exactly as Orca drives them
(`onExit -> destroy()`), handles bucketed by NT object type:
relay spawn (no useConptyDll) 225 -> 285 (+1 Process +2 File/term)
after 219 -> 219 FLAT
desktop spawn (useConptyDll) 239 -> 439 (+1 Process +2 Thread +5 File/term)
after 235 -> 395 (+2 Thread +4 File/term)
The desktop residue is a separate defect in the `useConptyDll` branch of
`WindowsPtyAgent.kill()`, which disposes the conout worker only from an
`_outSocket.on('data')` handler -- and no data arrives after the shell has gone.
Fixing that line as well takes the desktop to 222 -> 222 FLAT, but it lives in
the `kill()` hunk owned by F23, so it is left to that change.
Refs F24.
* fix(pty): dispose the conout worker when a Windows shell exits by itself
Second, independent defect on the same self-exit path, and the larger half of
the desktop's leak. The `useConptyDll` branch of `WindowsPtyAgent.kill()`
disposed the conout worker only from an `_outSocket.on('data')` handler -- and
once the shell has gone no more data ever arrives, so the worker was never
disposed. The non-DLL branch three lines above already disposed unconditionally,
which is why only the desktop (the only spawner that sets `useConptyDll`) hit it.
Measured on Windows 11, 20 cycles, handles bucketed by NT object type, totals:
self-exit, relay spawn 225 -> 285 now 219 -> 219 FLAT
self-exit, desktop spawn 239 -> 439 now 222 -> 222 FLAT
explicit kill, relay spawn 225 -> 285 now 219 -> 219 FLAT
explicit kill, desktop spawn 235 -> 395 now 219 -> 219 FLAT
Neither fix alone is enough on the desktop: the pseudoconsole close is worth
+1 Process +1 File per terminal, this dispose +2 Thread +4 File.
The relay asset (config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs)
deliberately gets no counterpart: the relay takes the non-DLL branch, where the
dispose is already unconditional. Its reconstruction table needs the new hunk
though, or un-applying the desktop hunks no longer yields published node-pty.
Taken over from F23 at win-relay-qa's request after they verified that the
desktop never executes the non-DLL branch F23 was scoped around.
Refs F24.
* fix(pty): harden PtyKill against a failed handle duplication and a missing DLL
Both from review of #18635.
DuplicateHandle's result was dropped. On the live explicit-kill path a failed
duplication left hShellDup null, which the guard below could not tell apart from
the self-exit case, so TerminateProcess was skipped and the shell kept running
after its pane closed -- a worse outcome than the handle leak this patch exists
to fix. The failure now terminates through handle->hShell under the lock, where
it is valid and where TerminateProcess does not block. The only cost is that the
rare path kills before the console closes instead of after.
LoadConptyDll is now resolved BEFORE any baton state is touched, matching what
PtyConnect already does for the same reason. It throws when conpty.dll is
missing, and a throw after consoleClosed was set would strand the pseudoconsole
permanently: the retry finds the work claimed and does nothing.
Also corrects three comments the earlier commits made stale:
- the ptyJobMutex note still said PtyKill reads the table unlocked
- PtyListJobProcessIds said the baton is gone once the shell exits; it now
outlives the shell, and the nulled hJob is what makes the answer null
- windows-pty-job.ts said node-pty drops its handle record on exit
Re-measured on Windows 11 with the rebuilt binary, 20 cycles, all four paths
still flat: self-exit relay 219->219, self-exit desktop 222->222, explicit-kill
relay 219->219, explicit-kill desktop 219->219. Both explicit-kill runs report
22/22 shells exited, so the kill still lands.
Refs F24.
|
||
|
|
766b5b153c |
fix(relay): release the ConPTY conin handle after teardown, not before it (#18601)
A Windows SSH relay leaked one Windows File handle per terminal, for the life of the relay process, across reconnects. node-pty's `kill()` flips `readable` on the conin and conout sockets and destroys neither; `_cleanUpProcess` destroys `_outSocket`, so only conin is stranded, and it wraps a real named-pipe handle from `fs.openSync(term.conin, 'w')`. The obvious fix -- and the one config/patches/node-pty@1.1.0.patch ships for the desktop -- releases it at the top of the branch, before `_getConsoleProcessList()` forks and before the native kill. Measured against a real Windows SSH host, that is three times worse than leaving the leak alone: teardown aborts partway, the forked console-list agent is never reaped, and both pipe handles stay alive. Releasing it at the end of the branch instead is flat. 20 spawn/kill cycles, handles bucketed by NT object type, identical numbers standalone and through a real relay: published node-pty File +1/terminal, Process flat desktop patch placement File +2/terminal, Process +1/terminal released last (this) File flat, Process flat `windowsTerminal.js` takes the desktop's error-listener hunks verbatim. The conin listener is not what fixes the leak -- adding it alone changed nothing -- but it is what keeps a pipe error retiring one terminal instead of the host. The desktop patch has the early placement and therefore the regression, measured against its exact installed tree. Correcting it there needs its own verification on a Windows desktop build, so the trees diverge on this one hunk deliberately and a test pins that so a future patch sync cannot copy the bug back. |
||
|
|
b85510f3a9 |
fix(terminal): warn about remote work when closing the window or quitting (#18593)
The native window-close warning was built from a local-only pty set: any worktree with a connectionId was dropped whole, and any remote runtime pty was filtered out. A build, test run, or agent on an SSH or Orca Remote host was therefore structurally invisible to it, on every platform. The quit path skipped the check entirely (#524), so remote work got no prompt at all. Route both paths through the same probe the tab-close guard uses, so the two cannot drift, and keep the verdict vocabulary of the SSH execution boundary: only a host that answers "no children" suppresses the warning. An unreachable host is `unverifiable`, never `exited`, so it warns rather than quitting silently — with its own copy, because "could not reach the host" is a different claim than "processes are running". Quit still ignores local ptys, preserving #524: quitting is an unambiguous instruction to end this machine's processes, but not to end execution on someone else's, which a bounded relay grace period will SIGKILL once the countdown expires. The probe budget is 1.5s (vs the tab guard's 4s) because quit is time sensitive; expiry raises the prompt, so an unreachable host costs a click rather than the 15s RPC timeout or a silently orphaned build. |
||
|
|
f36c03e84a |
fix(windows): make the install-dir ACL repair rescue the launch it runs in (#18361)
* fix(windows): repair the poisoned install-dir ACL before the window, not after The install-dir LPAC ACL poison (electron/electron#51761) still costs every affected machine at least one crash: the probe that detects it is setImmediate-deferred and answers 0.9-3.0s in, while createMainWindow runs synchronously in the same frame and its renderer dies at init 48-1373ms later. - Persist the poison verdict the moment the probe reports it, and await the repair (bounded at 20s) before any window is created on a launch that already carries the marker. - Do not engage the GPU safe-graphics fallback while the install-dir ACL verdict is poisoned or still outstanding. Safe graphics does not rescue a poisoned tree, and --in-process-gpu removes the GPU child, erasing the sibling-death evidence that identifies the shape (4 field reports landed in 'misc' this way). - Clear the safe-graphics marker once the repair lands, so a repaired machine stops launching software-rendered for the rest of that build. - Give the repair marker a bounded retry budget: it was written on failure and matched regardless of outcome, so one transient failure pinned a machine to 'marker-hit' for the life of that version. * test(windows): pin the install-dir ACL repair against the real icacls binary * fix(windows): stop the install-DACL verdict from outliving the evidence Adversarial review round 1. Five blocking findings, all addressed. 1. gpu-lifecycle guard had only a source grep (green with the polarity inverted). The stated justification -- that gpu-lifecycle's import graph cannot be driven in-process -- was wrong: mocking `electron` plus `@electron-toolkit/utils` imports it fine. Replaced with gpu-lifecycle-install-dir-acl-guard.test.ts, which drives the real handleGpuChildCrash against a stub tracker. All four cases go red when the guard is flipped to `if (!isInstallDirAclSuspect())`. 2. A clean probe verdict retired the on-disk marker but not the in-memory `poison` verdict, so a machine the probe just proved healthy kept suppressing the GPU safe-graphics fallback and kept the dialog accusing the install folder -- permanently, since a `status:'failed'` probe deliberately keeps the marker. A positive clean reading now latches `installDirReadClean`, drops the verdict, and outranks a repair result that lands after it (a 'failed' from a repair with nothing left to fix must not re-accuse). 'repaired' is kept: it is not a contradiction and it is what tells the user to reload. 3. `noteWindowsInstallDirAclProbePending()` ran on every `openMainWindow` while the probe is once-per-process, so every tray/second-instance reopen armed a 15s window in which `recordGpuCrash` was never called at all -- on healthy machines. `probeWindowsInstallDirAcl` now reports whether THIS call dispatched, and only a dispatch arms the grace window. 4. The pre-window ordering guarantee was defeatable and untested. `focusExistingMainWindow` opens a window whenever there is none and the app is ready -- true for the whole 20s gate, which is exactly when a user double-clicks the shortcut again. Added a `canOpenWindow` seam (same 'pending' semantics as the existing `!app.isReady()` case) wired to `isBlockingInstallDirAclRepairInFlight()`, plus windows-install-dir-acl-startup-wiring.test.ts pinning the await ahead of both window-creation paths and both new call sites. 5. windows-install-dir-acl-repair.win32.test.ts was absent from the pr.yml win32 allowlist, so it ran nowhere. Added. Also from the non-blocking list: - The repair no longer clears a `userConfirmed: true` safe-graphics marker; "keep safe graphics" is a user choice, not Orca's automatic latch. - `repairWindowsInstallDirPackageAcl` now reports its dispatch too, so a second entry into the gate resolves immediately instead of eating the full 20s budget waiting on an `onDone` that is never coming. - The gate is wrapped in try/catch/finally, matching the contract the probe documents as mandatory for anything upstream of window creation. Rebutted, not applied: - "Gate should be conditioned on app.isPackaged." A dev launch only carries the poison marker if a dev launch actually probed that tree and found the signature, in which case the dev renderer is dying the same way and the repair is exactly what is needed. The adjacent `isPackaged` check guards a packaged-only early-window optimisation, not a correctness boundary. - "Fold the poison marker into the repair marker's `outcome`." They answer different questions with different lifetimes. The repair marker is a retry budget (`attempts >= 3` disables the repair for that version) and is never cleared; the poison marker is cleared by a successful repair and by a clean probe. A `'pending'` outcome written before the attempt would bump `attempts`, so three launches killed mid-repair would permanently disable a repair that never once ran icacls to completion. * fix(windows): keep counting GPU crashes while the install-DACL verdict is pending Adversarial review round 2. Both blocking findings addressed. 1. handleGpuChildCrash early-returned on isInstallDirAclSuspect() BEFORE recordGpuCrash, so the crash left no trace in the 30s rolling window. The suspect window is armed on every win32 non-serve launch, and the field bundles put it at 0.8-1.7s after main_window_created on hosts whose DACL is clean (matchesPoisonSignature=false) -- squarely inside the 2.1-6.2s bad-driver bursts this repo already pinned in gpu-crash-fallback-field-sessions.test.ts. A healthy machine with a failing driver could lose an entire coalesced burst and never engage safe graphics. The crash is now always recorded; only the engagement consults the verdict, and it waits for the verdict rather than acting on the suspicion (waitForInstallDirAclVerdict, resolved by the probe's onDone or by the existing 15s grace, whichever lands first). Deviation from the review's suggested shape, deliberately: awaiting the verdict before persisting anything reintroduces the exact race gpu-fallback-engagement.ts documents -- Chromium aborts the whole browser process on the 6th GPU crash, ~1.3s after the 3rd, which is less than the probe takes to answer. So the unconfirmed marker is written up front and withdrawn if the verdict comes back poisoned. A machine killed mid-wait still comes back software-rendered, and its marker is unconfirmed, which is the state the repair's own clear already retires. gpu-lifecycle-install-dir-acl-guard.test.ts now drives the real GpuCrashFallbackTracker and the real engagement path (the restart prompt firing is the signal) instead of a stub tracker, and covers the case the previous suite could not express: a burst that lands entirely inside the pending window still engages once the probe reports clean. Four reverts go red -- restoring the pre-record guard (2 tests), dropping the wait, dropping the post-wait re-check, and dropping the pre-wait marker write (2 tests). 2. The round-1 evidence block quoted commits, a test name and pass counts that no longer exist, and its real-icacls Windows run predated the commit that rewrote the gate. Re-run at this commit; counts and the live-Windows result are restated in the handoff rather than carried forward. Also from the non-blocking list: - 'marker-hit' conflated "already repaired" with "retry budget spent", because hasMarkerFor matches outcome === 'repaired' too. The result now carries alreadyRepaired, and the recovery maps that to stage 'repaired' -- so a launch killed between a successful repair and its marker clear no longer tells the user the folder needs an administrator, no longer latches isInstallDirAclSuspect() for the session, and does retire the poison marker. Not applied, with reasoning: - "clearGpuFallbackMarker narrowed to userConfirmed === false leaves the target population software-rendered after a repair." The summary was overstated and is corrected, but the narrowing stands: a userConfirmed marker now requires a clean DACL verdict, because the restart prompt that writes it is exactly what the gate above withholds while the install is a suspect. The population this family targets can no longer reach confirmMarker while poisoned. - "writeInstallDirAclPoisonMarker re-stamps on a budget-exhausted machine forever." True, but on that machine the tree really is still poisoned and the gate resolves immediately ('skipped', no icacls spawn, no 20s wait), so the marker is telling the truth. Retiring it would be wrong; only a clean probe reading should. * fix(windows): register the real-icacls spec and stop its teardown racing icacls Two ratchets were red: - windows-lane-tree-removal-boundary: the win32 spec's afterAll used raw rmSync on a tree two icacls.exe children had just rewritten DACLs on, which is the EPERM race removeTreeSync exists for. - win32-test-lane-registration: the spec was in the pr.yml argv but not in WINDOWS_PACKAGE_TESTS, so a future diff touching only test files would not select package_windows and the spec would self-skip on ubuntu and report success. * fix(windows): re-arm the GPU fallback latch when the install-DACL verdict withholds it recordGpuCrash reports the threshold crossing exactly once and latches `engaged`. handleGpuChildCrash consumes that report before consulting the DACL verdict, and installDirAclClearsGpuFallback then discards it — so nothing could ever engage safe graphics again in that process. A machine whose tree the repair fixes and whose driver is genuinely broken stayed hardware-accelerated through an unbounded crash loop, with no prompt and no marker. disengage() releases only the one-shot latch; the crash window is untouched, so a real driver burst is still never erased. Test is RED without the re-arm. * fix(windows): keep the safe-graphics marker while an install-DACL repair is in flight The gate dispatches a repair without arming the probe clock, so waitForInstallDirAclVerdict() returns immediately and the withdrawal deleted the marker inside Chromium's FATAL window (crash 6 lands ~1.3s after crash 3, well inside the 20s gate). The process then died mid-repair, spent no attempt, and relaunched hardware accelerated into the same gate — spawning the same GPU children, FATALing again, forever. Hold the marker while poison.stage is 'pending' so that launch comes back software rendered and the next gate runs to completion. Still not engaged this launch, so --in-process-gpu does not erase the sibling-death evidence. A terminal verdict has no next step to rescue, so it still withdraws. Both new tests are RED without the retention. * fix(windows): stop a repaired marker outranking a fresh poison verdict The probe reads the install DACL and finds it poisoned; `startRepair` dispatches; `markerHitFor` sees a repair marker recording `outcome: 'repaired'` for the same installDir+appVersion and reports `alreadyRepaired`, which the recovery module maps to stage 'repaired'. So the launch that just proved the tree poisoned runs no icacls, deletes the poison marker that arms the next launch's pre-window gate, clears the suspect flag so `--in-process-gpu` can engage on a tree safe graphics cannot rescue, and tells the user "Orca repaired the permissions." Reachable whenever the tree is re-poisoned after one successful repair of the same version, and whenever a repair reports success without clearing the tree — the silent icacls no-op this module exists to document. A DACL reading taken this launch now outranks the marker: `probeConfirmedPoisoned` stops `outcome: 'repaired'` short-circuiting the repair. The attempt budget still bounds it, so an unrepairable tree does not re-spawn icacls forever. The pre-window gate does not set the flag — it acts on a marker from an earlier launch, not on evidence of its own, so a recorded repair still outranks it there. Also drives the GPU-fallback re-arm test through a repair that actually completes 'repaired', rather than a later clean probe, which is the route the review exercised. * fix(windows): make the pre-window ACL gate act on the poison evidence it fired on The gate fired on a poison marker — an earlier launch's DACL reading that nothing has retired — but withheld `probeConfirmedPoisoned` from the repair, so a repair marker recording an older success still short-circuited it. On the three-launch shape the gate exists for (repair succeeds; tree is re-poisoned; the next launch's probe records the poison but dies before writing its repair marker) the gate ran no icacls, deleted the poison marker that arms every later gate, un-suspected the tree so --in-process-gpu could engage, and told the user "Orca repaired the permissions." `applyInstallDirAclProbeVerdict` then swallowed that launch's own reading behind `if (poison) return`. Both callers of `startRepair` hold outstanding poison evidence, so the flag is now unconditional (renamed `poisonEvidenceOutstanding`) and `marker-hit` means only that the attempt budget is spent. The probe guard is narrowed to an in-flight gate repair: a reading taken after the gate finished re-arms the poison marker and downgrades a claimed repair. Also: withholding safe graphics now ends with the repair budget. A machine whose attempts are spent while the signature persists was denied safe graphics on every launch for the life of that appVersion — and had its marker deleted each time — including the healthy installs the probe's flag-blind ACE match over-matches, where the driver really is broken. Non-blocking, same lane: re-read `isQuitting` after the up-to-15s verdict wait, and skip the recovered-launch prompt when the ACL gate retired the marker read before whenReady. * fix(windows): stop a timed-out gate repair outranking a later poison reading The gate's 20s budget expires while icacls runs on under its own 120s cap, so the probe can read the tree poisoned while that repair is still in flight. Its success claim then deleted the poison marker, un-suspected the tree and told the user their permissions were fixed. The reading is now latched and outranks it. * fix(windows): stop a gate repair claim pre-empting this launch's probe reading Round-7 adversarial findings, both driven against the real modules: - isInstallDirAclSuspect returned false the moment the pre-window gate set stage 'repaired', short-circuiting ahead of the probe-pending grace check. The GPU children die 48-1373ms after window creation while the probe answers 0.9-3.0s in, so an icacls that silently no-opped (exit 0, tree untouched) opened exactly that interval to --in-process-gpu on a still-poisoned tree - and a 'keep safe graphics' answer then pinned a userConfirmed marker no later repair may clear, with the poison marker already deleted so no later launch gates. The claim now stays provisional until this launch's probe corroborates it or the grace window lapses. - A probe reading that disproves a 'repaired' claim re-armed the poison marker but never restored the unconfirmed safe-graphics marker the claim had cleared, so the next launch relaunched hardware-accelerated into the re-armed gate. The clear is now captured and handed back on disproof. * test(windows): pin the nested and update-inherited grants against real icacls The live spec asserted the grant landed on the root-level module file only. It now also pins that the flagless /T pass reaches a nested file carrying its own protected DACL (the shape app.asar.unpacked and node_modules have), and that a file written after the repair inherits the (OI)(CI) root grant - the stated reason that grant form exists. * fix(windows): keep the recovered-launch prompt silent while the tree is the suspect Round-8 fresh-eyes finding, driven against the real modules: the prompt re-read the marker the pre-window gate may have retired, but never consulted isInstallDirAclSuspect() - so after a FAILED gate (tree still a live suspect, window blank behind the 10s reveal fallback, Keep as both defaultId and cancelId) a 'keep it' answer pinned a userConfirmed marker no later repair may clear, on the exact victim class the repair cannot help. The guard now covers both gate outcomes; staying silent leaves the marker unconfirmed, which a successful repair still retires. --------- Co-authored-by: Orca Worker <orca-worker@localhost> Co-authored-by: OrcaWin <alpha-eng@stably.ai> |
||
|
|
1c4c6b7fec |
perf(startup): stop queueing window creation behind the proxy apply and i18n (#18436)
* perf(startup): stop queueing window creation behind the proxy apply and i18n
Three independent, measured startup wins, all free:
1. Park the initial Chromium proxy apply on `mainProcessState` instead of
awaiting it mid-`initializeReadyFoundation`. `setProxy` still starts at the
identical moment; the default-session request guard (which holds, not
cancels) is what actually fences fetchers on it, so only window creation
stops waiting. Runtime launch still awaits it before the desktop relay and
before every headless-serve fetcher.
2. Run `initializeMainProcessI18nAndMenu` concurrently with
`initializeMainProcessRuntimeLaunch`. Nothing in window creation reads a
translated string or the native menu.
3. Load `emojibase-data` in main through `createRequire` on first use instead
of a static import, keeping 166 KB of JSON off `out/main/index.js` and its
~2 ms parse off every launch. The renderer keeps its eager copy unchanged.
out/main/index.js 7,210,071 -> 7,040,147 bytes. No renderer behaviour changes.
* fix(packaging): ship the emoji shortcode dataset main lazily requires
app.asar carries no node_modules, so main's bare requires resolve only out of
Resources/node_modules. emojibase-data is a devDependency and is not in the
packaged runtime allowlist, so the new createRequire in
deferred-emoji-shortcode-dataset.ts threw MODULE_NOT_FOUND in every packaged
build — breaking sanitizeWorktreeName, and with it workspace creation.
Copy the single 166 KB dataset (not the 49 MB package root) into
Resources/node_modules, and gate every createRequire'd bare specifier in
src/main against the packaged resource plan. verifyPackagedMainRuntimeDeps
cannot catch these: the bundler renames the require binding.
* test(proxy): fail CI when a main-process fetcher escapes the default-session guard
The hoist relies on installElectronProxyRequestGuard(session.defaultSession) holding every app-owned request until the persisted proxy lands. Nothing enforced that every fetcher actually lands on defaultSession. Two source-anchored rules do now: no net.fetch/net.request may name a session/partition, and every non-net .fetch( call site is counted against an allowlist.
* test(proxy): close the shorthand and chained-receiver holes in the fetch call-site audit
The audit caught `net.request({ session: x })` and `ident.fetch(`, but not the two
shapes a real regression is just as likely to take: the `{ url, session }` shorthand
that both `net.request` overloads accept, and a receiver with no bare identifier
(`session.fromPartition(...).fetch(`, `ctx.session.fetch(`). Rule 1 now also matches
the shorthand key; rule 2 scans every `.fetch(` and excludes only a literal
`net`/`globalThis`/`global` receiver. Audited counts are unchanged (2/2/1).
* fix(startup): scope the deferred emoji loader to the projects that own it
TS6307: the composite web project lists src/main/ipc/worktree-logic.ts, which
now imports the deferred dataset loader, and the shared lazy test reached into
src/main from a project that has no src/main files. Add the loader to
tsconfig.tc.web.json and move the cross-project case into a src/main test.
Also close the last two review gaps: gate the runtime-RPC startup failure
dialog (the only launch-phase translateMain reader) on a published i18n
barrier so a concurrent i18n phase cannot leave a non-English user with the
English fallback, and let the fetch call-site audit match `net.fetch (url)`.
|
||
|
|
49d6d35b16 |
feat(i18n): add French UI locale
foXaCe <290678+foXaCe@users.noreply.github.com> |
||
|
|
04ae62202a |
fix(ssh): close the macOS relay's per-terminal pty fd leak (#18534)
The relay asset from #17920 only rewrote the forkpty `default:` call site, which sits in the `#else` arm of PtyFork's `#if defined(__APPLE__)`. macOS takes `pty_posix_spawn`, so the asset had never patched anything a Mac executes -- and `applyNodePtyMasterCloexecPatch` returned 'fixed' for any non-Linux host without running the script at all, which is what publishes a tree to the shared native-deps cache. Stock `pty_posix_spawn` opens up to three throwaway ptys to push the real master off fds 0-2 and never closes them: the cleanup loop is `for (; count > 0; count--)`, but the first `posix_openpt()` in a running process already returns >= 2, so it breaks with `count == 0` and the body never runs -- and where it does run it closes `low_fds[count]`, never `low_fds[0]`. One orphaned /dev/ptmx fd per terminal, for the life of the relay. Ports the `low_fds` fix and the Apple-branch `pty_cloexec(master)` call from the app's `config/patches/node-pty@1.1.0.patch`, byte-identical, and runs the gate on darwin. macOS needs a different build layout than Linux: it has no `build/` at all, so the fallback moved aside is `prebuilds/darwin-<arch>` -- which is also what makes node-pty's install script fall through from "prebuild found" to node-gyp -- and the compile writes a `build/Release` the loader checks first. Verification is per-platform too: Linux's leak is inheritance (/proc), macOS's is self-held (lsof). Also corrects the asset's claim that "macOS re-opens the tty through uv_tty_init's cloexec dup". Measured false: FD_CLOEXEC is not set on the master. What protects it is POSIX_SPAWN_CLOEXEC_DEFAULT, one option away from gone since uid/gid drops libuv back to fork()/exec() -- so the master is now marked there too. Measured on darwin-arm64, one PTY per open/close cycle in a relay-shaped dir running the relay's own commands: before cycle:ptmx 1:1 2:2 3:3 ... 10:10 (10 after a settle) after cycle:ptmx 1:0 2:0 3:0 ... 10:0 (0 after a settle) Linux re-verified in docker node:22: inherited before, isolated after, `already-patched` on the second run. Refs #17915 Refs #8362 |
||
|
|
98e77ef1a7 |
feat(mobile): structured native Codex chat (#18074)
* feat(mobile): finalize structured native Codex chat * fix(mobile): close structured chat lifecycle gaps * wip(mobile): fence stale structured inventory and bound operation-id retention Fence local structured-session inventory and subscription responses with a sync generation so a toggle-off clear, reconnect restore, or retry cannot apply a mirror from a superseded instance. Bound mobile ambiguous operation-ID retention at 128 with unmount cleanup. Staged on the reconcile branch only: the sync module is now 312 lines and needs a real split before this can reach the PR head. * fix(ci): split the structured session-tabs sync and give static analysis mobile types The local structured session-tabs sync module outgrew the 300-line cap once it took on generation fencing, so split it along its real seams instead of raising the cap: the generation/cursor fence, snapshot projection, snapshot apply, inventory refresh, and the subscription loop. The original path stays as a barrel so no importer moves. Repoint the host-session-mirror settle census at the apply module, which owns two receipts now — the snapshot it mirrors in, and the toggle-off teardown that retracts what it published. The teardown receipt is named rather than anonymous so the pin says which direction it settles. The changed-code quality gate lints mobile files and resolves their types from mobile/node_modules, but mobile is a separate pnpm project that the root install never populates, so every mobile type degraded to an `error` type and the gate reported phantom findings. Install mobile dependencies in static analysis when the diff touches mobile, gated on a new classifier output. * fix(mobile): let a slow capability handshake still reach connected The mobile capability update is an advisory whose result is discarded, yet an unanswered one was fatal while an explicit rejection was tolerated. A 5s timeout on the direct client force-closed the socket, and on the relay path it failed `confirmResume` before `connected` was ever published, so a consistently slow link redialled forever. Both paths now share one helper that settles every ambiguous outcome (timeout, mid-flight drop) like a rejection and rejects only when the frame never reached the wire — the one case nothing else recovers from, since the socket's own desync force-close is gated on already being connected. The generation guard still keeps a replaced session from connecting. Retained structured-session operation ids were capped at 128 with oldest-first eviction, but every retained id belongs to a send whose outcome is unknown, so eviction turned a user's retry into a second message on the host. Bound the map by expiry against the id's own embedded timestamp instead, mirroring the host's operation ledger, so no id is released while the host would still honour it. Also give the mobile CI install the root install's lockfile drift guard (mobile's lockfile carries patchedDependencies a silent rewrite would drop), gate mobile_dependencies on should_run, and key the pnpm store cache on both lockfiles. * refactor(mobile): extract the relay pending-request registry The merge composed two independently-sized changes — this branch's capability handshake settle and main's dial-stage tracking — pushing the relay session file to 304 lines against a 300 cap. Neither side broke it alone. Move the in-flight request registry (id generation, tracking, settlement, and reject-all with its delivery-ambiguity marking) into RelayPendingRequests, matching the existing collaborator pattern alongside RelayDialStageTracker and RpcSessionLivenessWatchdog. No behavior change. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
f13f2472c6 |
fix(i18n): distinguish Duplicate from Copy in Simplified Chinese
Reviewed and approved by Codex. |
||
|
|
8262fb147f |
fix(i18n): extract translateSearchKeyword calls so settings-search keywords reach en.json
Reviewed and approved by Codex. |
||
|
|
aa78d4af17 |
fix(release): restore version and harden staging confirmation
Resolves release scan blockers STA-6611 and STA-6612. |
||
|
|
3eec77c11a |
chore(cloud): add the relay fence broker, ops console, Terraform root, scripts, and 24 cloud-* workflows (#18413)
Phase 6 of the relay split: the relay's deploy/operate surface moves under cloud/ with 24 cloud-* workflows gated on ORCA_CLOUD_OPERATIONS_ENABLED, the Cloud SQL rollout lease action, the relay Terraform root (dual-accept identities for both repositories), scripts, docs, CODEOWNERS, and a terraform validate job in Cloud Verify. |
||
|
|
968dbd905f |
perf(renderer): take the English catalog and the xterm WebGL addon off the boot graph (#18326)
* perf(renderer): take the English catalog, xterm WebGL addon and emoji data off the boot graph The renderer's boot graph — the entry chunk plus its 331 modulepreload links, all fetched and evaluated before first paint — carried three payloads nothing needs at that moment. `en.json` (644 KB) was an eager i18next resource, but every renderer string goes through `translate(key, fallback)` and `en` resolves that inline default, so most of the catalog was dead weight. The renderer now bundles a generated `en-runtime-required.json` holding only the 2,583 of 13,828 entries a default cannot reproduce: plural-suffixed keys, keys whose catalog value differs from a call site's default, and keys no call site references with a literal default. `en.json` stays the translator source and the input to the four lazy catalogs. `@xterm/addon-webgl` (243.6 KB) and `emojibase-data` (170 KB) are now primed right after the React root renders instead of statically imported. The load stays eager and `attachWebgl` stays synchronous — it reads the resolved constructor — so no terminal ever falls back to the DOM renderer for a frame. `isPluginPanelTabKey`/`isQualifiedPluginKey` move to schema-free sibling modules, re-exported from `plugin-manifest.ts`. This evicts the plugin manifest schema graph from the boot chunk but measures ~0 KB, because six other shared modules still put zod on the boot path. Boot graph: 332 chunks / 5107.2 KB -> 336 chunks / 4161.5 KB (-945.7 KB, -18.5%). A new ratchet parses the built index.html and fails if `en.json`, `@xterm/addon-webgl` or `emojibase-data` is preloaded again; it runs at the end of every `build:electron-vite`. * chore(i18n): pin the generated English subset to LF and mark it generated * fix(i18n): make the runtime-catalog gate merge-robust and prime emoji data in tests CI builds the merge of a PR with main, so a byte-for-byte comparison against a committed generated file fails the moment any unrelated PR adds a translate() call — which is what happened here. The check now asserts the property that actually matters instead of byte equality: every runtime-required entry is shipped, and nothing shipped disagrees with en.json. Entries that stopped being required are dead weight, never a wrong string, so they are reported and tolerated. Failures now name the offending keys rather than saying "stale". The generator itself was already deterministic (plain code-unit sort, no locale collation, order-independent set construction); a test now pins that a reversed call-site walk produces byte-identical output. Test fixes for the catalog prune and the deferred emoji load: - browser-search / NativeChatSupportedAgents asserted key presence on the renderer's runtime resource. The durable contract is en.json — the renderer deliberately no longer bundles entries a call site default reproduces — so they assert against the translator catalog. - Four emoji tests typed a shortcode in the same tick as mount, before the catalog the hook primes on mount resolves. Not reachable by a human; the tests now await the prime. * revert(renderer): keep the emoji shortcode catalog statically imported Deferring emojibase-data introduced a window that did not exist before: until the dynamic import settled, getPrimedEmojiShortcodeEntries returned [], so exactShortcodeIndex built an empty map and replaceCompletedWorkspaceEmojiShortcode returned null — leaving a typed `:wink:` in the field literally, and persisting it as the workspace display name. Pre-change the shared catalog was statically imported, so the first call at any tick returned full data. The window is reachable by anything that dispatches input in the same task as the field's mount effect — Playwright/CDP in the e2e suite and agent automation both do, and the WorktreeMetaDialog test failure was exactly that, producing 'Feature 😉' instead of 'Feature 😉'. Nothing that resolves a shortcode can be async without that race, and a wrong persisted name is not an acceptable trade for 166.7 KB, so the deferral is reverted rather than papered over in the tests. The boot-graph ratchet drops its emojibase-data probe and records why. Boot graph: 5108.9 KB -> 4329.9 KB (-779.0 KB, -15.2%), down from -945.7 KB. * fix(terminal): make the deferred WebGL addon load recoverable and refit on late attach Two defects the deferral introduced, neither possible with a static import. A failed load latched the DOM renderer for the whole session. `.then(onOk, onError)` settles fulfilled, so the memoized promise was cached forever with a null constructor: attachWebgl's re-prime got the cached promise back, and resetTerminalWebglSuggestion — the documented "GPU setting changed, retry" path — could not clear it either. The rejection path now clears the memo, latches the queued panes the way a failed construction does so they retry at a recovery boundary rather than every frame, and caps attempts so a genuinely missing chunk is not re-fetched forever. The recovery boundary re-arms it. The queued-attach drain skipped the refit. Every other late-attach path pairs attach with a refit because the grid was measured under DOM cell metrics and WebGL floors the device cell width. Post-deferral, openTerminal's attachWebgl queued and returned, the initial fit rAF then measured DOM metrics and sized the PTY from them, and the addon attached with no refit — a persistently narrow PTY and an unpainted right gutter, not a one-frame flicker. Both paths now go through one attachWebglAndRefit pairing so they cannot diverge again. Regression tests cover both, and each was verified to fail without its fix. The addon-load state machine moves to terminal-webgl-addon-loader.ts and the viewport presentation helpers to pane-viewport-present.ts, keeping pane-webgl-renderer.ts under the 300-line budget without a suppression. |
||
|
|
ddb13a10f7 | perf(agent-status): memoize pane routing, cache the freshness minimum, stage one clone per transaction (#18323) | ||
|
|
6cd477a2f1 |
test(e2e): un-rot the SSH freeze repro and probe two failure modes nothing covered (#17940)
Test-only. No production code. ## The freeze repro was rotted in three ways, not one #16764 tracks four stale call sites. There were three separate problems: 1. **Stale call sites** — `execInTerminal` gained a `ptyId` and `splitActiveTerminalPane` gained a direction. (`startDockerSshRelayTarget`'s missing `testInfo` was the third; #18257 has since landed it on main.) 2. **It connected before session restore settled**, so the seeded tab never bound to a remote PTY and the terminal sat on "Connecting…" forever. 3. **It could never have passed, even once.** It waited for a one-shot `READY:` line through a 4000-char terminal window while its own 2 KB-every-8 ms flood buries that line within ~16 ms. Readiness is now keyed on the repeating `BG:` flood marker, which is strictly stronger — it proves the pane is streaming rather than merely started. It now runs end to end and prints a measurement instead of dying on a call site: ``` [freeze-repro R2] hiddenFloodMaxLagMs 2.1 bulkOpenMaxLagMs 41.5 interactionProbeMs 53.6 softFreeze false hardFreeze false ``` **It is still not CI-gateable, and the exclusion comment now says so.** The same spec on the same commit measured `bulkOpen 2575.6ms / interaction 3464.2ms` on a GitHub ubuntu runner against a 2500 ms soft budget — a ~60x spread on the number the budget reads, with the relay still streaming. That is the budget failing, not the product. The earlier draft of this comment claimed "repaired and passing", which was true only of the host it was measured on; gating this needs a host-relative oracle, not a bigger constant. ## New: a half-open link is judged, not wedged The fixture image has no `iptables` and the container has no `NET_ADMIN`, so `docker pause` is used instead — a harder case, because the container's TCP stack keeps ACKing: no FIN, no RST, and the socket looks perfectly healthy. Only an application-level probe can detect it. ``` [half-open] {"verdict":"reconnecting","verdictMs":25135,"budgetMs":90000} ``` Nothing in the suite covered the failure mode behind the "SSH hangs until I restart Orca" reports. ## New: resource accumulation measured on the remote host 6 terminals, then 5 reconnect cycles, counted on the container itself: ``` open: pts 1->6 (exactly 1/terminal), relay fds 25->30 (exactly 1/terminal) reconnect: pts flat at 6, relay procs flat at 1, node procs flat at 3 ``` `leakedMasterFdCount` is now **asserted**, not merely recorded. It counts PTY master fds held by non-relay processes: without `FD_CLOEXEC` a master is inherited by every later child, so terminal k adds k of them — the triangular signature measured as 15 across 5 terminals before the fix. #17914 patched the app and daemon and #17920 shipped the same patch to the relay host, and both are now on main, so the correct value is 0 and the probe holds it there: ``` baseline leakedMasterFdCount 0 6 terminals leakedMasterFdCount 0 (holders: only relay.js, n=6) reconnects leakedMasterFdCount 0 across all 5 cycles ``` Any growth here means the relay's node-pty rebuild did not take on that host, which is exactly what a remote-host probe exists to catch — and it is the half of #17914's claim that no unit test can reach. ## Routing Both new probes are claimed by `run-ssh-docker-e2e.mjs` (a Docker-gated spec no runner names self-skips everywhere and still reports green) **and** by the `ssh-terminal-source` route in `pr-e2e-source-routing.mjs`, so they run when the relay and SSH code they guard changes rather than only on a scheduled lane. |
||
|
|
6c66487fca | ci: checkout PR head for reusable E2E (#18230) | ||
|
|
f737f3499f |
fix(relay): stream an oversized fs.listFiles reply instead of refusing it (#17954)
Opening Orca's own checkout over SSH cannot list its files in one response frame. 22,617 tracked paths average 58 characters, so the 20,001-row page the client asks for serializes to 1,223,415 bytes — past `DISPATCHER_CONTROL_QUEUE_MAX_BYTES`, so `sendResponse` demotes it to the `legacy-response` lane, where an unrelated producer backlog can refuse it as an opaque `ResponseOverCapacity`. Break-even is around 49 characters of average path; any `packages/<name>/src/...` monorepo is over the line. Picking a ceiling to refuse at does not fix that, it just moves where it shows up and refuses listings that would have been delivered. `__streamResponse` already exists for exactly this on the git methods, and it is its own negotiation in both directions: an old client never sends it and gets the plain array on the legacy-response lane as before, and an old relay ignores it and answers plainly, which the client detects by the sentinel marker being absent. So fs.listFiles opts into it — no new method, no new opcode, nothing to advertise — and the size of a listing stops being a correctness question. The response-stream registry becomes one per relay, shared by FsHandler and GitHandler. A second registry is not an option and the header of git-response-stream.ts says why: a client keys reassembly on `streamId` alone, so two would hand out the same id and cross-feed chunks, and only the handler that registers `git.responseAck` can credit the window a pump parks on. Also declares `maxResults` on the runtime-RPC `files.listAll` and forwards it. The mechanism "the client names its cap, so a full page reads as truncation" was wired only on the Electron IPC hop; web and mobile were saved incidentally by `remoteFileContentBudget` defaulting the cap inside `listRuntimeFiles`. A new optional field is additive in both directions (wire rule 1). The new Docker-gated spec is claimed by run-ssh-docker-e2e.mjs. The sharded e2e lanes set no ORCA_E2E_SSH_DOCKER, so a Docker-gated spec that no runner names self-skips everywhere and still reports green — pr-e2e-gate-contract enforces that. Closes #12547 |
||
|
|
f37d2fec97 |
fix(linux): land the reviewed Linux packaging stack on main (#18100)
* fix(linux): give the CLI one entrypoint by extracting the AppImage once
* refactor(linux): trim AppImage CLI registration seams
* test(cli): assert registration lock serialization
* fix(linux): fence AppImage terminal shim mounts
* fix(linux): accept extracted AppImage runtimes with APPDIR only
* docs(linux): make headless AppImage extraction runnable
* refactor(linux): import bundled launcher directly
* fix(linux): reclaim superseded AppImage payloads and packaged symlinks
Pruning removed 3215 of 3216 files from a superseded generation and always
stranded resources/app.asar, leaking ~105 MB per version update. Electron's
asar shim reports a *.asar file as a directory, so the recursive remove tried
to rmdir a real file and failed with ENOTEMPTY; the .catch(() => {}) hid it.
Reproduced end to end on Ubuntu 24.04: 519M -> 623M across one update, and
519M again once the payload is actually reclaimed.
removeExtractedAppImagePayload holds process.noAsar for the removal, counted
so overlapping removals cannot hand the shim back early, and the prune site
now warns with the path instead of swallowing the rejection. All three
removal sites use it -- staging cleanup and displaced roots leaked the same
way.
Also reclaim symlinks left by a packaged deb/rpm install, which the
extracted-cache-only rule turned into a hard conflict on a deb -> AppImage
migration, and name the remedy in the conflict error.
* fix(linux): bound the CLI registration lock wait
`retries: 1000` caps the attempt count, not elapsed time, so at up to 1s per
attempt an IPC-driven registration could hang ~16 minutes against a wedged
holder with no feedback.
A legitimate holder is bounded by the extraction timeout, so wait that plus
slack and then fail with a message naming the lock file, rather than hanging.
`maxRetryTime` is forwarded verbatim to the `retry` package by proper-lockfile.
* fix(linux): stop re-extracting the AppImage on inode metadata churn
The extracted-payload cache key hashed ctime alongside dev/ino/size/mtime.
ctime moves on any inode metadata write -- `chmod +x`, which every AppImage
user is told to run, plus `chown`, an ACL or SELinux relabel, and a backup
restore -- none of which alter a byte of the payload.
Measured on Ubuntu 24.04: `chmod +x` leaves dev, ino, size and mtime
identical and moves ctime alone, so the key changed and the next launch paid
a full ~519 MB re-extraction and a multi-second stall to rebuild a payload it
already had, then pruned the old generation.
Key on content identity instead. An in-place content change moves mtime and
almost always size; a replacement moves the inode. The existing
replace-in-place test still passes.
* fix(linux): stop CLI commands from falling through to Chromium startup
* refactor(cli): remove redundant command membership check
* test(cli): cover command-named project selectors
* fix(cli): redirect the open-url command before startup
* test(linux): cover AUR serve wrapper flags
* fix(linux): tighten CLI launch detection
* fix(linux): respect CLI flag value boundaries
* fix(linux): strip injected Chromium switches from CLI args
* fix(linux): report a missing display instead of dying in uv_close
* refactor(linux): read display locks without a preflight race
* fix(linux): preserve unverified external displays
* chore: format reliability gate manifest
* test(packaging): split runtime resource checks
* fix(linux): fail serve when no display is available
* fix(linux): do not treat a lockless X socket as a dead display
An X server writes its lock beside its socket and both survive a crash
(verified against Xvfb under SIGKILL), so a socket with no lock was never
left by a crashed server. It is an endpoint published from elsewhere: a
container bind-mounting only /tmp/.X11-unix, WSLg, or a foreign PID
namespace. Declaring those dead made the desktop gate exit(1) on displays
that work, with no workaround, and the serve gate refuse to start.
Liveness now splits by ownership. A foreign DISPLAY trusts a lockless
socket; Orca's own :99 does not, because removeStaleDisplayArtifacts
unlinks the lock before the socket and so manufactures that state itself --
adopting it would resurrect the orphan-socket bug and stop the cleanup from
self-healing. The stale-lock rejection is unchanged.
Also correct four doc statements this behaviour falsified.
* fix(linux): fail closed when a stale socket blocks the Xvfb rebind
Readiness only checked that /tmp/.X11-unix/X99 exists. A stale socket we
could not unlink still exists after our own Xvfb refused to bind, so Orca set
DISPLAY to a dead server and Chromium died in Ozone init.
Measured on Ubuntu 24.04 against the pre-fix build: with a leftover :99
socket and no lock, serve exits 139 (SIGSEGV), the socket inode is unchanged
before and after, and no lock is recreated -- it neither cleaned up nor
respawned. To a user that is a crash, not a misconfiguration.
This is reachable in the documented topology, where orca-xvfb.service has no
User= and runs as root while serve runs as User=orca: /tmp is sticky, so the
orca uid cannot unlink a root-owned socket, rmSync fails, and Xvfb exits with
the display already active.
Readiness now requires the display to actually be live -- our socket plus a
lock naming a running process -- so the same state reports an unusable
display and exits 1 with the existing diagnosis.
* fix(linux): recognise abstract X sockets and inherited Wayland fds
Two display setups this gate could not prove were refused outright, and on the
desktop path that is app.exit(1) with no workaround.
An X server may bind only the abstract namespace (`@/tmp/.X11-unix/X0`), which
leaves no filesystem socket to stat. Abstract addresses are kernel-owned and
vanish the moment the owner exits, so an entry in /proc/net/unix is proof of a
live server -- no lock file needed and no stale entry possible. Verified on
Ubuntu 24.04, where 139 such addresses were present.
WAYLAND_SOCKET is an already-connected fd handed over by the compositor, so
there is no path to stat and WAYLAND_DISPLAY may be unset entirely. Its
presence is the display.
Both are consulted only after the filesystem-socket check fails, so no
existing verdict changes.
* fix(linux): never treat Orca's own display number as a foreign endpoint
Recognising a lockless X socket as live is correct for an endpoint published
from elsewhere -- a container bind mount, WSLg -- because an X server writes
its lock beside its socket and both survive a crash. It is wrong for
VIRTUAL_DISPLAY_NUMBER, because Orca's own teardown unlinks the lock before
the socket and so manufactures that exact state.
The managed branch was already strict, but a caller that sets DISPLAY=:99
explicitly takes the foreign path and skipped it, accepting a dead display
left by Orca's own interrupted cleanup. Route the managed number through the
strict probe on both paths.
Found by an adversarial audit of the asymmetry introduced earlier in this
branch; the documented systemd topology is unaffected because its Xvfb writes
a real lock.
* test(linux): add a packaged-artifact contract for the CLI launch paths
* test(linux): avoid buffered serve readiness detection
* test(linux): signal AppImage serve owner directly
* test(linux): tolerate readiness timeout boundary
* test(linux): add startup margin to shutdown oracle
* ci(linux): give package contracts timeout headroom
* fix(ci): route all Linux packaging contract changes
* test(linux): poll shutdown readiness without tail leaks
* test(linux): bound shutdown cleanup grace
* test(linux): assert on CLI output, not the harness's own control lines
run-cli-case.sh echoes `RESULT status=N case=<name>`, and the two cases named
*-skills asserted `expectOutput: 'skills'`. That substring was satisfied by
the case name in the harness's own line, so 2 of 8 cases asserted nothing
about the command -- gutting `skills` entirely would still have gone green.
Control lines are now excluded before matching, and both cases assert the
rendered help header, which only real help output produces. Verified on an
Ubuntu 24.04 host: 8/8 still pass against a stack-tip AppImage.
Also register the gate in reliability-gates.jsonc, which #15085 added a CI
Docker gate without. Red/green is recorded from a stock release AppImage
failing 4 of 8, three of them at status 133 (SIGTRAP).
* fix(linux): require static AppImage runtimes (#17319)
* test(linux): reject a wrong-architecture native binary at packaging time
Cross-building the arm64 slice on an x64 host silently packed an x86-64
`pty.node` -- the rebuild logged "Forcing native rebuild for linux-arm64" and
shipped the host's binary anyway. Every gate here inspects symbol versions,
which are perfectly valid on the wrong architecture, so nothing noticed.
Observed on a Raspberry Pi 5: the packaged app loaded, then failed with
"Failed to load native module: pty.node", and the launch contract reported
3 of 8 cases crashed rather than naming the cause. Swapping in the aarch64
`pty.node` took the same build to 8/8.
Compare ELF `e_machine` against the slice being packaged and fail with the
offending path. Checked before the glibc pass, because a wrong-architecture
binary's symbol versions are valid but meaningless and would send the reader
down the wrong path.
Release CI builds arm64 on a native runner, so this guards local and future
cross-builds rather than a shipped artifact.
* test(linux): judge per-arch vendored binaries against their own path
The first CI run of the architecture gate failed the x64 package job on
`@parcel/watcher-linux-arm64-glibc/watcher.node`. That binary is arm64 on
purpose: the package ships every architecture and its loader picks the match,
so its presence in an x64 build is correct.
Judge a binary against the architecture its own path names, falling back to
the slice when the path names none. That keeps the case this gate exists for
-- `bin/linux-arm64-*/node-pty.node` holding an x86-64 binary, which is what
shipped to a Raspberry Pi 5 -- while letting multi-arch dependencies through.
Dry-run over the real dependency tree flags nothing for either target arch.
* fix(linux): move deb/rpm update installation outside Orca (#17318)
* fix(linux): complete deb/rpm package metadata
* fix(linux): preserve CLI link during package upgrades
* docs(linux): document local RPM build prerequisites
* fix(linux): move deb/rpm update installation outside Orca
* fix(updater): preserve Linux recovery across stale events
* fix(updater): fence stale downloaded events by active target
* fix(updater): preserve active Linux package recovery
* test(linux): keep workflow order assertion in scope
* test(updater): assert stale recovery stays silent
* fix(updater): preserve Linux package recovery after checks
* refactor(updater): keep Linux marker message with status
* fix(linux): describe the right manual update path for deb/rpm hosts
A remote host installed from .deb or .rpm now reports
manual-service-update-required, and the guidance told the operator to
"update through the service manager that starts this server" -- which is
correct for unsupported-headless-serve but wrong for a package install,
where nothing about the remedy involves the service manager.
Say both, keyed on how the host was installed.
* docs(linux): document orcad update restart safety
* docs(linux): scope restart census omissions
* docs(linux): use absolute service CLI launcher
* fix(serve): validate in-process serve options before startup (#17683)
* fix(linux): stop offering updates a distro-managed install cannot apply (#17918)
Closes #17702.
The resources/package-type marker is authoritative but never checked against
the host, so any repackager that unpacks Orca's .deb -- AUR, Nix, a container
rebuild -- inherits `deb` verbatim. Install feasibility was then computed
after a ~165 MB download, so those users got check -> download -> a card
promising an install command -> a dead end.
Validate the marker against the host: a deb/rpm marker with no matching
package manager in the trusted directories means a package manager owns this
install. This reuses the exact lists and resolver that
buildLinuxPackageInstallCommand already loops over, so a false positive is
impossible by construction -- any host flagged here would have failed with
no-package-manager after the download anyway. The gate only moves that
verdict earlier. Verified across Debian 12, Ubuntu 24.04, Arch, Fedora 40 and
openSUSE Leap: no false positive on a real deb host, correct on every
repackaging host.
The release is still reported, because the user does want to know 1.4.194
exists and to update through their distro; only the download path is closed.
`externallyManaged` is an additive optional field on the existing `available`
status, so older paired clients decode it unchanged. downloadUpdate() refuses
authoritatively, since main owns this verdict rather than the card, and
unwinds any pinned-build state first -- a Linux pinned jump resolves to
'release', and stranding isPinnedBuildActive would silently kill every
background check for the rest of the process.
Note the fix the issue suggests cannot work: electron-updater builds a
PacmanUpdater whose doDownloadUpdate looks for a .pacman asset Orca does not
publish, then dereferences undefined.
* style(cli): restore prettier wrapping on install error copy
* test(linux): re-pin the child-process ratchets and the batch-shim allowlist after the merge
|
||
|
|
aa3ae6f56e |
fix(ssh): close the pty master fd leak on relay hosts too (#17920)
* fix(ssh): close the pty master fd leak on Linux relay hosts The app gets the FD_CLOEXEC patch through pnpm patchedDependencies (#17914); the relay installs stock node-pty from npm, where no pnpm patch reaches. Linux is where that matters -- it is the only relay platform that takes forkpty()'s no-atomic-O_CLOEXEC path, and it is also the only one that already compiles node-pty at install time, so the fix costs a second compile rather than a first. Ships the patch as a relay asset applied like the existing Windows console-list one, and rebuilds only after the probe has proven node-pty loadable. The rebuild is non-fatal by construction: the working build is moved aside first and moved back on any failure, a failed attempt drops a skip marker so the compile is attempted at most once per relay directory, and the caller swallows the whole step. macOS and Windows relays never run it. Measured on node:22 with a relay-style npm install: before, the master is cloexec=false and shows up as `26 -> /dev/pts/ptmx` in both a later pty child and a later child_process child; after, cloexec=true and neither child sees it. Closes #17915. * test(ssh): feed the cloexec patch exec to the hand-rolled namespace fixtures These sequences are positional, so the new Linux-only patch exec swallowed the READY slot and every install/repair case timed out waiting for the relay. * fix(ssh): patch the pty master before publishing the shared native-deps tree * fix(ssh): refuse to publish a native-deps tree whose cloexec patch did not take |
||
|
|
34999e328e |
fix(orcad): stop demanding a spawn-helper only macOS builds (#18122)
node-pty declares the spawn-helper target inside binding.gyp's OS=="mac" block and pty.cc execs it only under __APPLE__. Asserting it on `!== 'win32'` made every Linux orcad boot degraded with spawn_helper_missing while its terminals worked fine. Route all four sites through one shared `usesNodePtySpawnHelper` predicate: the precondition verdict, the prebuilt slot install, the +x repair, and the prebuilds build script (which threw outright on a Linux slot build). Fixes #17844 |
||
|
|
0dbe9d0504 |
test(ssh): dockerized relay fault injection with verdict assertions (#18017)
* test(ssh): add a dockerized SSH fault-injection lane with four fault shapes The existing SSH reconnect specs all reconnect by calling ssh.disconnect() then ssh.connect() - a clean cycle the client knows is coming. Nothing covered the faults the reconnect machinery exists for. Four shapes, each documented with why it is not the others: killing sshd's per-connection forks (transport dies, relay survives), `docker pause` (silence with TCP still established), SIGKILLing every relay.js (the only fault where `exited` is the correct verdict), and a 48MB flood with nobody attached. The relay-kill case is the one that makes the rest meaningful: every other case asserts the session survived, which only means something if a genuinely dead session is distinguishable. It is the only case where replacing the pane is correct, so it pins the boundary in docs/reference/ssh-execution-boundary.md rather than just testing reconnection. The `docker pause` case pins the other side of that boundary: after 30s of silence from a healthy host the pane keeps its PTY and its scrollback, because loss of contact is never evidence of death. No network-blackhole fault: reconnecting the fixture does not restore its published port mapping, so that fault is not reversible on this container and would strand the worker it ran on. * test(ssh): fixme the flood case pending #18018 It fails in CI on its first real run: the pane keeps its PTY and repaints, but a command run after the flood produces no output within the poll budget. Same shape as #18018 and not caused by this spec. The three verdict assertions around it stay enforced. |
||
|
|
3ae51076b1 |
fix(tooling): run oxlint gates without a Windows .cmd shim (#17894)
* fix(tooling): run oxlint gates without a Windows .cmd shim
`check:code-quality:changed` spawned `pnpm.cmd` without a shell, which Node
refuses under the CVE-2024-27980 mitigation, so the gate died with EINVAL
before linting anything. Resolve oxlint's own Node bin and run it under this
process's node instead — no shim, no shell, no quoting question — and add a
ratchet so the idiom cannot spread back into config/scripts.
* fix(tooling): validate the react-doctor diff base and widen the shim ratchet
`base` reaches cmd.exe unquoted on the shell fallback, so reject anything
outside a git revision before spawning. The ratchet matched only a handful of
runner names, which let `vitest.cmd` through even though config/scripts already
spawns vitest, playwright and electron-builder; match any batch-shim literal
instead, walk subdirectories, and cover tests/tools.
* docs(tooling): state what the shim ratchet and diff-base check miss
Both comments read as complete accounts of their guard's coverage. The revision
class rejects reflog syntax like HEAD@{1}, deliberately, since braces have no
business in a cmd.exe-bound argument; the ratchet misses a drive-lettered
literal because a colon is not in its class. Say so beside the template-literal
ceiling already noted.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
|
||
|
|
0c9c3c00cf |
test(ci): ratchet Windows-gated tests into both registration lists (#18047)
* test(ci): ratchet Windows-gated tests into both registration lists
PR CI has one windows-2022 job running a curated explicit file list. Every
other job runs on ubuntu, where a Windows-gated suite self-skips and reports
success -- so an unregistered Windows-gated file executes on no machine and
passes green with nothing to tell the author.
Scans every test file for the win32 suite-level gate spellings in use plus the
.win32.test.* filename, and asserts each one appears in BOTH the
"Test Windows-specific boundaries" vitest argv and WINDOWS_PACKAGE_TESTS: the
classifier decides whether the job runs, the argv decides whether the file
runs. The eight already-unregistered files on main are held in a shrink-only
debt list.
* fix(ci): detect compound win32 gates in the lane-registration ratchet
The gate matcher anchored its argument on the closing paren, so
`runIf(platform === 'win32' && hasAddon)` was not matched at all -- the
guard excluded real Windows-gated files by accident of a regex rather
than by design, and would have missed a compound gate on a file that
genuinely needed registering.
Match the condition followed by `)` or `&&`, and resolve named flags from
their assignment in the same file, so `RUN_REAL = platform === 'win32' &&
env…` used as `runIf(RUN_REAL)` is detected whatever the flag is called
and whichever polarity it was written in. That replaces the hardcoded
`isWindows`/`IS_WINDOWS`/`isWin32` names, which guessed polarity from a
name; an imported flag stays undetected and is now documented with the
live example. `||` compounds are rejected on purpose: they can run off
Windows.
Ten env-opt-in suites surface as a result. They are win32-gated but also
require an `ORCA_REAL_*` env var, so registering them would not make CI
run them; they go in MANUAL_OPT_IN, whose entries are asserted to be
genuinely compound and env-gated so the list cannot become a quiet
parking spot.
Also: reuse `scanSourceTree` instead of a fifth divergent walk in the
repo (its docblock records the incident where a hand-rolled walk scanned
`tests/e2e/.cross-version-checkouts/`), adding an `extensions` option so
it can see `.mjs`; strip comments so prose about a gate is not a gate;
skip `mobile/`, which `classifyPrJobs` can never report as registered;
assert exactly one `windows-2022` job, the premise the guard rests on;
cap growth of both grandfathered lists; and test that the self-exemption
covers nothing but this file.
Corrects two docblock claims that were false: that nothing in the repo
computes a gate indirectly (three files did), and that a compound gate's
registration was asserted while only its execution was not (neither was).
* fix(ci): make the manual-opt-in exemption prove the env read reaches the gate
`requiresEnvOptIn` proved the file MENTIONED an env var, not that the gate
DEPENDED on one, so `runIf(platform === 'win32' && hasAddon)` in a file
that happens to read `process.env.RUNNER_TEMP` parked as manual. That is
the native-addon-bytes shape -- a test CI could run -- and only the cap
number stood in the way. Now the win32 check must be compound and one of
its other conjuncts must read `process.env` itself or name a const that
does, which still accepts all ten listed suites.
The compound clause guarding that hole was itself unasserted: deleting it
left every test green. Two fixtures close it, including an env read on the
same line as a bare gate, which is the case that makes the `&&` do work
rather than decorate.
Split FLAG_ASSIGNMENT by polarity. One shared `&&` lookahead was right for
`===` (a second conjunct narrows) and wrong for `!==` (it widens), so
`p = platform !== 'win32' && x` used as `skipIf(p)` read as Windows-only
though it runs on Windows and on POSIX when `x` is false. The literal form
was already rejected; routing it through a flag flipped the answer.
Widen the one-lane assertion from a `windows-2022` equality test to any
`runs-on` that could land on Windows -- `windows-latest`, a label array, a
`{ group, labels }` object -- treating an unresolvable `${{ }}` expression
as Windows so it fails closed.
Docblock: the case-level count is now deliberately approximate. The
reviewer measures 26 against this guard's 31; the figure moves with which
gate spellings are counted, and the policy does not rest on it.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
|
||
|
|
ee82feb776 |
fix(build): pin config/scripts LF so Windows can run their tests (#18056)
core.autocrlf=true ships in the Git-for-Windows system config, so a fresh Windows checkout materializes config/scripts/*.mjs with CRLF. Vite's SSR transform finds the shebang with /^#!.*\n/, and \r is a JS regex line terminator, so the pattern misses on CRLF: the hoisted import/export preamble lands at offset 0 ahead of the shebang, which then defeats the code[0] === '#' guard that blanks it. A literal #! survives into the middle of the module and every suite importing the script dies at load with SyntaxError: Invalid or unexpected token. Eight suites were unrunnable on Windows. .gitattributes already pinned eight of these scripts individually; replace those with one glob over the directory so the pin does not have to be remembered per file, and add a ratchet that fails when a shebanged script is left on the platform default. Co-authored-by: Orca Worker <orca-worker@localhost> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
ff1031186c |
ci(release): make Windows release gates deterministic (#18067)
* ci(release): keep Windows signing gate deterministic * test(release): skip oversized Windows cache fixture * ci(release): keep flaky Windows skill suite non-blocking |
||
|
|
fdfe354045 |
test(relay): bind test WebSocket servers to loopback
A control-handshake test that expects a timeout was instead getting
'Unexpected server response: 401' about once in fourteen runs. A slow machine
cannot turn a timeout into a 401 -- that needs a real HTTP response, so the
connection was reaching a different server.
new WebSocketServer({ port: 0 }) binds the wildcard address while the client
dials 127.0.0.1. On macOS those differ, and with SO_REUSEADDR a foreign process
can hold the more specific 127.0.0.1:P and win the connection. Caught live: a
wildcard bind took port 52584, which a running Orca app already held on
loopback, and Orca answered the probe. A listener that checks a token answers
401.
Ten constructions across seven files now pass host: '127.0.0.1', so the
reservation covers the address the client dials and a duplicate bind is refused.
Adds a ratchet, because this is not authors forgetting a convention: all 30+
.listen(0, ...) sites already pass '127.0.0.1', while 7 of 7 ws constructions
did not. ws accepts { port } alone and binds the wildcard silently, so nothing
told them. The guard pins the wildcard count, and pins separately at zero the
option shapes it cannot read -- spreads and variable option objects fail rather
than being exempted, and a recognized-construction floor catches the matcher
going blind, which otherwise reads exactly like a clean tree.
mobile/scripts/mock-server.ts stays on the wildcard deliberately: a phone
reaches it over the LAN.
|
||
|
|
4efc86a33c |
feat(app): open Markdown files from the OS in the floating workspace (#17906)
* feat(app): open Markdown files from the OS in the floating workspace Registers Orca as a Markdown handler on macOS, Windows and Linux, and opens an OS-handed .md/.markdown/.mdx file as a floating-workspace editor tab — the one editor surface that needs no project. Works cold-start and when Orca is already running. Main buffers the paths and both pushes to a live renderer and answers a pull on renderer mount, mirroring SkillShareDeepLinkState. The buffer is only released once delivery is possible: the renderer's pull is what proves its ui:openMarkdownFiles listener is attached, because a push into a window whose renderer has not subscribed is dropped by Electron with no error. Both the push and the pull restore an undelivered batch, and a renderer reload clears the latch so the fresh renderer re-proves itself. Paths are stat'd and proven to be files before authorizeExternalPath sees them. Windows association is registered by hand in the NSIS include rather than through electron-builder's `fileAssociations`: app-builder-lib emits APP_ASSOCIATE, whose first line overwrites Software\Classes\.md's default value with no backup — silently taking .md from whichever editor owns it, for every existing user on their next update — and APP_UNASSOCIATE never restores it. The hand-rolled registration is additive (ProgID + OpenWithProgids + SupportedTypes) and leaves the user's default alone; verified end to end on a real Windows 11 host. Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com> Co-authored-by: Jaydev <java-jaydev@users.noreply.github.com> Closes #10138 * fix(os-open): register the new listener in the IPC inventory, and guard a non-array payload CI caught two things the local run did not. useIpcEvents-lifecycle.test.ts is an inventory of every App-lifetime IPC listener and the exact order they register in; ui.onOpenMarkdownFiles now appears there, positioned after the workspace-shortcut bridge's last listener, which is where it actually registers. Chasing that failure surfaced a real gap: the pending-open payload crosses the preload boundary, so a stale or mismatched preload can resolve with something that is not an array, and reading .length off it threw inside the promise chain instead of failing at the boundary. Array.isArray now gates it, with a regression test. |
||
|
|
519af49a58 |
fix(dev): keep the shared Electron dist writable for the dev app
pn dev crashes on macOS in any worktree that adopted the shared Electron dist. publishSharedElectronDist marks the cache entry read-only, which hardlink sharing needs, but clonefile preserves mode -- so the dist lands 0555, the dev runner copies it into out/electron-dev unchanged, and the first plutil -replace on Info.plist fails with a permission error. The shipped zip has that file at 0644; on disk it is 0555, so the mode is ours, not upstream's. copyPrivateTree now restores write permission. Its contract is a private tree the caller goes on to patch, and its one production caller is the dev runner. The test that should have caught this ran the wrapper with stdio: 'ignore', so a hard crash presented as a bare 20s timeout. It now captures the wrapper's output into the failure message, and waits long enough for the two synchronous swiftc builds and a codesign --deep over ~280MB that precede the assertion. |
||
|
|
da4a83bd22 | fix(linux): give the CLI one entrypoint by extracting the AppImage once | ||
|
|
2be53521a7 |
refactor(task-page): fold the task page into one provider-grouped tree
The page had two competing splits: an Aug-25 folder split that the Aug-30
oversized-surfaces split stranded, and the 47 flat files that replaced it. The
orphaned tree had no non-test importers, yet eight ratchet files still asserted
against it, so their invariants stopped constraining shipping code -- which is
how six regressions reached main unnoticed. Those ratchets were repointed and
the regressions fixed earlier; this removes the tree they were guarding.
Moves the live files into task-page/{github,gitlab,jira,linear} and drops the
now-redundant prefix, matching the new-workspace sibling.
Makes the source-family walker recursive first: it listed a single flat
directory, so moving the files under it would have emptied the family and turned
every ratchet built on it into a no-op without failing.
|
||
|
|
b94a65a4fc |
fix(lint): preserve TaskPage effect suppressions after split
(cherry picked from commit
|
||
|
|
2e30187560 |
feat(dev): sweep the backlog of idle dev Electron bundles (#17803)
* fix(dev): make reclaim report real sizes on Windows and keep setuid intact Two bugs found by running the reclaim script on real Linux and Windows hosts. The size report shelled out to `du`, which does not exist on Windows, so every worktree measured 0 bytes and the script reported nothing reclaimable on the platform with the largest dist (374MB). Walk the tree in Node instead. makeTreeReadOnly chmod'd files to a flat 0o555, which clears setuid. On Linux that would silently strip the bit from chrome-sandbox if a developer had run the usual `sudo chown root && chmod 4755` workaround -- and under hardlink sharing it would strip it from every worktree and the cache at once. Clear the write bits and nothing else. Measured after the fix: 7.30 GiB across 23 worktrees on one Windows host and 18.31 GiB across 56 on another, both previously reported as 0. * feat(dev): sweep the backlog of idle dev Electron bundles out/electron-dev holds one ~275MB patched Electron.app per branch title x Electron version. The dev runner already prunes them, but only inside the worktree it is starting and only when that worktree holds more than one bundle -- and a worktree almost always holds exactly one, so the sweep returns early every time and nothing ever reclaims another worktree's bundle. pnpm reclaim:dev-bundles sweeps across every worktree of the repo. Bundles are pure build output that pnpm dev rebuilds on demand, and rebuilding is cheap now that the Electron dist is shared. Reuses the runner's own staleness rules, so a bundle a live process is running from, or one whose build is still in flight, is never removed. Refuses to run at all if the process table cannot be read, rather than guessing. Measured: 120 bundles, 32.2 GiB, on one machine. Also guards both reclaim scripts behind a direct-invocation check; importing one for tests previously ran a full sweep at import time. |
||
|
|
e2f326cad7 | ci(release): prevent signing on workflow reruns (#17802) | ||
|
|
abe1d30881 | fix(dev): make reclaim report real sizes on Windows and keep setuid intact (#17800) | ||
|
|
fe0f2f9be7 |
perf(dev): share one Electron dist per repo instead of per worktree (#17664)
* perf(dev): clone one Electron dist per repo instead of per worktree Every worktree extracted its own ~295MB node_modules/electron/dist, measured at 69GB across 241 worktrees on one machine. Extract once per repository into <git-common-dir>/orca-cache/electron, then APFS-clone it into each worktree: copy-on-write, so the second worktree allocates ~0 bytes and still gets a real, private, writable directory. Hangs off install-electron-package-binary.mjs, inside the transaction it already uses to swap dist. Every cache path returns a boolean and false means "install normally", so non-APFS, cross-volume, corrupt entry, no Git, folder workspace and CI all keep today's behavior. No symlinks, no lifecycle changes. out/electron-dev's per-branch Electron.app copy clones too, via the same helper. Refs #13709 * perf(dev): share the Electron dist on Linux and Windows too Extends the shared dist cache beyond macOS APFS. Three mechanisms, strongest isolation first: macOS APFS cp -c private copy-on-write Linux btrfs cp --reflink private copy-on-write ext4 / NTFS hardlink + 0555 shared inodes, forced read-only Reflinks cover btrfs/XFS/bcachefs/ZFS but not ext4, and Windows block cloning is ReFS-only, so most Linux and effectively all Windows developers need hardlinks to get any saving at all. Extracted dist is 327MB on linux-x64 and 374MB on win32-x64, both larger than macOS. Hardlinks share inodes, so a write through one worktree would rewrite every sibling and the cache. Nothing in this repo writes inside dist -- every mutation replaces the directory via rename -- but Electron's own install.js extracts over an existing dist with O_TRUNC, and is reachable through `pnpm rebuild electron`. Publishing the entry read-only turns that from silent cross-worktree corruption into EPERM. Directories stay writable so the install transaction's renames and unlinks still work. out/electron-dev's per-branch Electron.app is patched and codesigned after it is copied, so it uses copyPrivateTree, which never hardlinks. Refs #13709 * test(dev): keep shared-dist tests honest across ext4 and NTFS Verified on real hardware: Ubuntu 24.04/ext4 (no reflink support, so the hardlink tier is the only thing that helps there) and Windows/NTFS. Three tests faked platform: 'darwin' while invoking the real mechanism, so they failed on Linux where /bin/cp -c does not exist. Mechanism selection is now asserted with injected stubs; real filesystem behavior is asserted against whatever the host actually supports. Windows maps chmod onto the read-only attribute alone, so a directory never reports 0o755 and a read-only file reports 0o444. Mode-bit assertions that encoded POSIX semantics are now behavioral (the tree stays removable), and the executable-bit assertion is POSIX-only -- confirmed on NTFS that a read-only hardlinked .exe still runs. * fix(dev): stop a losing publisher from discarding a good cache entry Greptile caught a TOCTOU in the shared Electron dist cache. Quarantining an invalid entry happened before sharing the replacement tree, which takes seconds -- long enough for a sibling worktree to publish a good entry that this one would then rename away. If the follow-up publish also failed, the cache was left empty and every worktree re-downloaded. Stage first, then re-validate immediately before the destructive rename, so an entry that became good during the share is kept. On a failed swap, restore the quarantined entry instead of leaving no entry at all: a stale entry still beats an empty cache, because the next publisher re-validates and replaces it. An entry that cannot be validated is never displaced, matching the pre-staging rule. Also covers the Electron upgrade path end to end: a version bump gets its own cache entry and leaves the previous one for worktrees still on the old branch. * feat(dev): add a script to share existing worktrees' Electron dists An install only shares when Electron is (re)installed, and rebuild-native-deps returns early when the package is already usable -- so a worktree that already has a working dist never reaches the sharing path and keeps its own copy until the next Electron upgrade. pnpm reclaim:electron-dists reports what it would share; --apply does it. Each worktree is converted behind a rename, so an interrupted run leaves a working dist either way, and any worktree that fails is left untouched. Measured on one machine: 677 worktrees, ~195 GiB reclaimable. * fix(dev): keep the reclaim script's error formatting type-safe |
||
|
|
69120d5402 |
ci(release): tolerate legacy tags without source maps (#17788)
* test(e2e): seed source control diff before opening panel * ci(release): tolerate legacy tags without source maps |
||
|
|
a5796ec8eb |
refactor(runtime): split OrcaRuntimeService and compatibility tests (#17605)
* refactor(runtime): split OrcaRuntimeService into focused modules
* test(runtime): cover admission tiers and strict worktree reconciliation
* fix(runtime): preserve owner and structured session visibility
* fix(runtime): port post-extraction compatibility fixes
* fix(runtime): preserve skill-share cancellation barrier
* test(runtime): update identity inventory after extraction
* fix(runtime): preserve hook transport environment cleanup
* fix(runtime): consolidate idle probe imports
* test(runtime): retire split file process allowlist entry
* fix(runtime): route child process types through shared boundary
* test(runtime): preserve worktree host metadata precedence
* fix(runtime): update extracted test seams
* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract
Audit follow-ups for the OrcaRuntimeService split:
- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
type checking. The split's linear mixin chain cannot express forward
references yet, so the existing suppressions are grandfathered; the baseline
may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
after the first statement, where TypeScript ignores it, so the module was
already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
argument. The split widened it to optional and patched the resulting error
with `stopConfirmed === true`; an omitted argument would have silently taken
the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
so one left out of the list would silently stop running.
* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped
Audit findings against the refactor's true base (
|
||
|
|
f116d2ca2a |
test(ci): retry Windows teardown EPERM and restart evaluate misses (#17780)
Restart-survival polls treated a recycled renderer as a hard failure. Wrap those evaluates so "Execution context was destroyed" is a pending miss. Windows package-lane teardowns after a force-kill used rmSync with force:true only, which does not absorb EPERM; put them on the shared maxRetries:8 policy. |