Commit Graph
588 Commits
Author SHA1 Message Date
Jinwoo-H 3753fac07e Merge branch 'skills-fix-env' into skills-optimization
# Conflicts:
#	config/scripts/generate-bundled-skill-guides.test.mjs
#	skill-stubs/orca-per-workspace-env.md
2026-09-04 17:34:30 -04:00
Jinwoo-H fe7d20f569 Merge branch 'skills-fix-linear-emu' into skills-optimization
# Conflicts:
#	config/scripts/generate-bundled-skill-guides.test.mjs
2026-09-04 17:34:01 -04:00
Jinwoo-H e3df3bbfe4 Merge branch 'skills-fix-cli' into skills-optimization
# Conflicts:
#	config/scripts/generate-bundled-skill-guides.test.mjs
2026-09-04 17:33:42 -04:00
Jinwoo-H b087216bcd Merge branch 'skills-fix-orch' into skills-optimization 2026-09-04 17:33:05 -04:00
Jinwoo-H b129932e6d Merge branch 'skills-fix-guards' into skills-optimization 2026-09-04 17:33:05 -04:00
Jinwoo-H a66bd9dc5a docs(skills): split per-workspace-env guide into a kernel plus references
Apply the Change findings from the skill review.

Correctness:
- relayGracePeriodSeconds now states that 0 is unbounded (the relay stays up
  until explicitly terminated) and names the accepted domain, 0 or 60..604800
  from EphemeralVmRecipeSshTargetSchema. Removed from the SSH exemplar so the
  omitted default and a written value cannot disagree.
- The SSH exemplar carries only the required fields. jumpHost/proxyCommand
  exclusivity is stated once, where the choice is made.
- The provisioned-root snippet fetches "$ORCA_REPO_URL", the remote Orca
  resolved the base ref against, not origin.
- portForwards entries name localPort/remoteHost/remotePort and optional label
  against the strict SavedPortForwardSchema.
- The free doctor gate is clear only with no fail and no warn; buildDoctorResult
  leaves ok true with warnings.
- The worked auth check uses the status command's exit code by default via a
  sentinel, since a provider CLI need not propagate a remote exit code. The grep
  recipe stays as the named fallback and matches a shell variable, so no
  pipefail/SIGPIPE hazard remains.

Structure:
- One outcome spine with a joined done bar, and one autonomy envelope replacing
  the four money restatements, the five checkpoint rationales, and Boundaries.
- Quick-start deleted; the numbered sequence is the single copy.
- Kernel keeps what must fire without a read; the four worked examples and the
  failure modes move to skill-guides/orca-per-workspace-env/references/.
- Description replaced with the review's proposal; the ORCA placeholder rule is
  stated once and the Claude Code bang prefix is fenced as a harness adapter.

The generator test's reference assertions are generalized per guide, and the
Vercel name-building pins are repointed to the file that now carries them.
2026-09-04 17:29:44 -04:00
Jinwoo-H 30d8dd6ce8 test(skills): report reference-routing mismatches as text, not arrays 2026-09-04 17:25:19 -04:00
Jinwoo-H 47e5e2ba68 docs(skills): correct linear and emulator skill guides
Emulator: drop the camera-injection verb (it does not exist in
src/cli/specs/emulator.ts), drop iOS permissions (the iOS backend
declares permissions: false so the bridge throws emulator_unsupported),
fix the Android permissions positional order and remove the nonexistent
list op, and delete the stale 'visual pane in development' status and
the 'once the attach/active flow lands' qualifier. State the wrapped-verb
and backend-capability conditions once, add an outcome spine, and drop
the ASCII diagrams and identity prose.

Linear: delete the 27-line Common Commands mirror of --help, replace the
verb-keyed unconfirmed-write rules with the payload-keyed condition that
covers every write verb, add a skill-level done bar for all five
branches, and move examples onto the ORCA placeholder.

All four descriptions rewritten off body-owned detail.
2026-09-04 17:25:06 -04:00
Jinwoo-H dca9336a4a test(skills): guard guide-to-CLI parity, description shape, references, and size 2026-09-04 17:24:12 -04:00
Jinwoo-H 1dabd8724d refactor(skills): single-author the shared discovery-stub blocks
The resolver ladder, the ORCA placeholder rule, the no-guessing paragraph,
and the older-binary fallback frame were authored eight times each and had
already drifted where they were re-copied. Move them into one fragment,
skill-stubs/_shared/cli-resolution.md, and have each per-topic stub mark
where they land with `<!-- shared: <id> -->`.

The projected skills/<name>/SKILL.md is byte-identical for all eight topics,
so no manifest revision moves.
2026-09-04 17:23:29 -04:00
Jinwoo-H 47e3bf4ac1 docs(skills): give orca-cli and computer-use an outcome spine and conditional references
Applies the Change findings from the orca-cli / computer-use skill review.

orca-cli guide:
- Delete the duplicated mobile-emulator tail and the second `Next Action`; the
  emulator now routes through one conditional-references row.
- State the handoff done bar once, and state the `terminal wait` gate with its
  failure direction beside the recipe: `terminal wait` prints an ordinary result
  envelope on timeout and signals the unsatisfied wait only through the exit
  code, so an agent could send a brief into a half-started TUI.
- State the one-agent-handle invariant once in Worktrees; the Terminals copy and
  the two restatements are gone.
- Drop the executable-resolution ladder the discovery stub owns and keep one
  placeholder rule, in the shape `skill-guides/orchestration.md` uses.
- Move the reconstructible command catalogs (browser, automations, artifact and
  skill publishing) behind `skills get orca-cli --full`; the routing paragraph,
  the untrusted-page-content rule and the artifact publish gate stay inline.
- 424 always-loaded lines to 260.

computer-use guide: promote the verification vocabulary to a done bar, and drop
its copy of the resolver ladder.

Repointed the generator's resolver-phrase assertions for these two guides to the
stub projections that now own the ladder, and generalized the bundled-reference
assertions beyond orchestration.
2026-09-04 17:23:11 -04:00
Jinwoo-H 610baee9d4 skills(orchestration): state the outcome spine's consumer and one positive-proof condition
Applies the Change findings from the orchestration skill review.

- Outcome names the next consumer (the requesting user) and the turn-end report
  contract, and states the positive-proof condition once beside Safe failure.
- The three wait/release gates cite that condition instead of carrying divergent
  case lists.
- Drops the kernel's third copy of the worker_done command; the runtime preamble
  owns it at the point of use and worker-contract.md owns it behind the gate.
- Drops messaging-and-gates.md's duplicate coordinator delivery loop block; the
  paragraph below it already states the --terminal delta as a condition.
- worker-start gets an ordered failure hatch naming failedStage and
  residualResources.
- Conditional references describes what --full returns instead of promising
  selective loading; the worker-contract gate row states the condition the kernel
  does not decide.
- Moves the worktree-selector form to placement-and-remote.md, where an exact
  selector is consumed, and aligns worker-contract.md on the kernel's
  capability-first question-TUI wording.

Pins move with the facts: the worker_done flag spellings to worker-contract.md's
recipe case, the worktree-id form to the placement reference.
2026-09-04 17:20:34 -04:00
Jinwoo-H e443ed63ae Merge branch 'fix-final-surface' into integrate-final
# Conflicts:
#	src/cli/bundled-skill-guides.ts
2026-09-04 15:44:27 -04:00
Jinwoo-H 4a09ba3ad7 docs(orchestration): say an empty check never means you were replaced
The worker contract calls an empty check a checkpoint rather than a failure, so
a fenced worker had no sentence telling it that consumer_fenced, not silence, is
how it learns the Task moved. Pinned next to the existing consumer_fenced pin.
2026-09-04 15:41:29 -04:00
Jinwoo-H 2b21ce5ce2 docs(orchestration): enumerate remote workers on the kernel stall path
A coordinator that never loads a reference read unverifiable for every
--on <environment> worker. Fits the existing wrap, so the 202-line budget
is unchanged.
2026-09-04 15:34:26 -04:00
Jinwoo-H 8e28b7de61 docs(orchestration): stop the kernel from looping on an informational nextAction
For a healthy in-progress worker the projection returns nextAction
{kind:'inspect', argv:['orchestration','worker-show','--dispatch',<same id>]}
with attention.requiresAction false, and the kernel told the coordinator to
follow the literal argv. Raises the kernel line budget 200 -> 202: the
paragraph had zero slack and no existing guidance was worth cutting.
2026-09-04 15:30:09 -04:00
Jinwoo-H a4f416f5b4 docs(orchestration): state that --types is a wake condition, not a batch filter
check --types without --wait returned an unmatched type. wakeTypes is an
existence probe in getOrCreateMailboxDelivery; the batch query that
follows has no type predicate, so a Delivery is never filtered. Behavior
is correct; only the guide and --help were silent.
2026-09-04 15:25:13 -04:00
Jinwoo-H cbe2d378fb docs(orchestration): name --include-remote on the guides' enumeration paths
The stall path told coordinators to enumerate with plain worker-list, but
a worker started --on <environment> reads unverifiable without
--include-remote. Also names page.nextCursor for fleets past 100 rows.
2026-09-04 15:24:31 -04:00
Jinwoo-H 9806af4258 Merge remote-tracking branch 'origin/main' into orchestration-v3
# Conflicts:
#	config/scripts/skill-description-length.test.mjs
#	resources/skills/current-manifest.json
#	resources/skills/snapshot-registry.json
#	skill-guides/orchestration.md
#	skills/orchestration/SKILL.md
#	src/cli/bundled-skill-guides.ts
2026-09-04 14:28:08 -04:00
Jinwoo Hong 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.
2026-09-04 14:27:07 -04:00
Jinwoo-H d571ab154f docs(orchestration): tell a fenced worker to stop instead of retrying check 2026-09-04 14:09:48 -04:00
Jinwoo-H ec589d8ce2 test(skills): guard every bundled SKILL.md description at the Agent Skills 1024-char limit
The orchestration description on main is 1,038 chars and conforming
installers reject it (#17935). This branch's rewrite already sits at 866;
the guard keeps it there. Guard adapted from #17959.
2026-09-04 13:55:29 -04:00
Neil 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.
2026-09-04 05:32:19 -07:00
Neil 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.
2026-09-04 01:56:42 -07:00
Jinwoo-H 6c3c0f6375 Merge remote-tracking branch 'origin/main' into integrate-fixes 2026-09-04 04:34:13 -04:00
Jinwoo-H 40aa364df7 docs(orchestration): correct the projection.attention path and restore routing phrases 2026-09-04 04:04:52 -04:00
Neil 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.
2026-09-04 00:51:21 -07:00
Jinwoo-H 5ab25102e5 docs(orchestration): restore the routing triggers to the skill description
The branch's rewrite dropped main's verbatim phrases ("hand off", "handoff",
"handover", "give this to another agent", "another worktree", threaded
messages, worker_done/escalation waits, decision gates, reading or waiting on
terminals) — the only text a model sees when choosing this skill. Restored in
both the kernel frontmatter and the identical stub, still shorter than main's,
and pinned by a routing test.
2026-09-04 03:23:41 -04:00
Jinwoo-H c78f40fdd0 docs(orchestration): require positive evidence of exit before ending a wait
The kernel's stall exit fired on "not live", which includes every unverifiable
arm — all of which are absence — contradicting the safety floor and the recovery
table. It now names the positive signals (exited liveness, the worker's own
observation of exit, a final agent turn with no worker_done) and states that
unverifiable never authorizes stop, abandon, retry, or release. Also corrects the
projection.* field paths the worker-list row actually nests.
2026-09-04 03:14:51 -04:00
Jinwoo-H dc6e0f6e47 docs(orchestration): give the kernel loop an exit condition and the missing commands
D1: name the two liveness layers (worker-list projection.liveness is the fleet
verdict, worker-show observation.status is PTY-only) and give the supervised
loop a bounded stall procedure instead of an unbounded wait.
D2: put worker-list, attention, requiresAction and nextAction in the loop and in
completion accounting.
D4: document request-show / --retry-request / terminal send --wait-submit.
D5: check names its caller with --terminal, never --from.
D7: give a dispatched worker a concrete follow-up read cadence.
D10: document the real folder-workspace route (project setup-existing-folder).
worker-start --spec is now the canonical loop's default.
Guidance pins are contracts via squash() instead of reflow-fragile prose.
2026-09-04 01:56:59 -04:00
Jinwoo-H 07fa28a3ae Merge origin/main into orchestration-v3
Runtime and rpc hunks from the pre-split monolith still need rehoming into
main's split modules; terminal.ts max-lines follow-up pending.
2026-09-04 00:49:43 -04:00
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>
2026-09-03 21:39:34 -07:00
Neil 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)`.
2026-09-03 21:19:06 -07:00
foXaCe 49d6d35b16 feat(i18n): add French UI locale
foXaCe <290678+foXaCe@users.noreply.github.com>
2026-09-03 17:32:59 -07:00
Neil 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
2026-09-03 16:13:07 -07:00
Brennan BensonandMerge Sim 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>
2026-09-03 15:19:26 -07:00
韦编三绝 f13f2472c6 fix(i18n): distinguish Duplicate from Copy in Simplified Chinese
Reviewed and approved by Codex.
2026-09-03 15:07:38 -07:00
Ilya Gusev 8262fb147f fix(i18n): extract translateSearchKeyword calls so settings-search keywords reach en.json
Reviewed and approved by Codex.
2026-09-03 15:07:34 -07:00
Jinwoo Hong aa78d4af17 fix(release): restore version and harden staging confirmation
Resolves release scan blockers STA-6611 and STA-6612.
2026-09-03 16:53:32 -04:00
Jinwoo Hong 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.
2026-09-03 06:55:14 -04:00
Neil 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.
2026-09-03 00:26:30 -07:00
Neil ddb13a10f7 perf(agent-status): memoize pane routing, cache the freshness minimum, stage one clone per transaction (#18323) 2026-09-02 23:19:38 -07:00
Neil 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.
2026-09-02 15:58:42 -07:00
Jinjing 6c66487fca ci: checkout PR head for reusable E2E (#18230) 2026-09-02 11:20:15 -07:00
Neil 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
2026-09-02 05:36:54 -07:00
Neil 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
2026-09-02 03:08:01 -07:00
Neil 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
2026-09-02 03:02:27 -07:00
Neil 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
2026-09-02 02:49:09 -07:00
Neil 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.
2026-09-01 23:35:47 -07:00
OrcaWinandOrca Worker 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>
2026-09-01 23:21:26 -07:00