mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
* docs: full design + context for agent status over WSL (STA-1515) Why hooks don't work on Windows+WSL (loopback transport gap + WSL-side installation gap), per-client transport map, the OMP-only fixes that shipped (7642/7641) and why they don't generalize, the recommended guest-resident relay over wsl.exe stdio mirroring the SSH relay plus WSL-side hook installers, alternatives considered, validation facts and gotchas from the 2026-07-08 Windows rig run, and acceptance criteria. Co-authored-by: Orca <help@stably.ai> * feat(agent-hooks): agent status over WSL — guest relay + WSL-side hook installers (STA-1515) Agent hooks have never worked from inside WSL: under default NAT networking, WSL's 127.0.0.1 is its own loopback, so every hook POST to the Windows listener dies silently, and hook configs were only ever written to the Windows home where WSL agents never see them. Transport: a hooks-only guest relay (src/relay/wsl-agent-hook-relay.ts) runs inside the distro, binds WSL loopback on the very port the clients were already given (host-issued token; EADDRINUSE falls back to :0 with endpoint-file re-coordination, which also covers mirrored networking), and forwards parsed envelopes over its own wsl.exe stdio into agentHookServer.ingestRemote — the same shape as the SSH relay. It exits when stdin closes so a freed Windows port can never be forwarded into a dead guest listener. Installation: the unchanged SSH remote hook installers run against an SFTP-shaped adapter whose primitives are home-scoped fs RPCs served by the relay, so all 14 managed agents' hooks land in the WSL home over the already-open channel with zero per-file wsl.exe spawns. Lifecycle: per-distro manager ensured from buildPtyHostEnv on every WSL PTY spawn (covers post-restart daemon reattach re-spawns), stale-bundle reinstall via exit 42, no-node-43 cooldown, bounded retry for wsl.exe 'Catastrophic failure (E_UNEXPECTED)', breadcrumbed failures. Zero per-client transport changes; listener stays Windows-loopback-only. Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): WSL relay link-death recovery + Codex runtime-home hook install (STA-1515) Follow-ups from the first Windows-rig validation of PR #7903: Link death: a mux protocol error or keepalive timeout could kill the host<->guest link while the guest relay stayed alive returning 204s — the manager stayed 'running' and every later envelope blackholed silently (the exact observed signature: Claude hooks POST 204, store never populates). wsl-hook-relay-link.ts now guarantees exactly-once death handling from either signal (mux dispose OR child exit); the manager breadcrumbs it, kills the child, and self-restarts after a short cooldown since a live agent session produces no new PTY spawns to re-trigger ensure. ORCA_WSL_HOOK_RELAY_DEBUG=1 traces each received envelope pre-ingest. A live integration test pins the full host chain: the real esbuild bundle over real child stdio through the real manager into a real AgentHookServer.ingestRemote, exact Claude POST shape. Codex: Orca launches WSL Codex with CODEX_HOME redirected to the managed runtime home (~/.local/share/orca/codex-runtime-home/home), so hooks installed to ~/.codex were never read. installRemote now accepts an explicit codex home (flat layout), threaded from the relay manager; the config.toml trust write is deferred while the file doesn't exist (the launch path seeds it only-if-absent — creating it first would cancel the seed), and the manager re-runs the byte-equality-idempotent installers on later ensures (30s throttle) to upsert trust once the seed lands. Also: WSL test suites now run on Windows dev hosts (fs-backed suites skip with rig coverage noted; manager suite uses a fixed POSIX home). Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): renderer ownership gate treats wsl:* connection ids as local (STA-1515) Round-2 rig finding: with the link fixed, WSL hook envelopes reached ingestRemote and the durable cache, but useIpcEvents.applyAgentStatus drops any status whose stamped connectionId differs from the owning repo's — 'wsl:<distro>' !== null for a local repo, so every WSL-relayed status died before setAgentStatus and notifications. wsl:* ids are transport provenance, not ownership: the gate now normalizes them to local via isWslHookRelayConnectionId (shared contract, also used by the relay link when stamping), while still rejecting WSL-stamped events against SSH-owned repos. Provenance stays stamped — it is what made this drop diagnosable. Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): adversarial-review hardening for the WSL hook relay (STA-1515) Four independent review lenses over the branch; all confirmed findings fixed before the next rig round: Endpoint identity (4/4 reviewers): the guest endpoint dir was keyed by the EPHEMERAL Windows hook port, so a daemon-surviving agent kept sourcing the dead port-P1 file after an Orca restart — breaking the restart-resume acceptance criterion and regressing shipped OMP recovery. Now keyed by a restart-stable instance key (hash of the Windows endpoint file path, crossed via ORCA_WSL_HOOK_INSTANCE): the restarted instance's relay rewrites the SAME file, which is exactly what re-coordinates survivors. Restart policy: every failure arms the restart timer (one failed relaunch no longer ends self-recovery), and the timer probes wsl --list --running first — wsl -d BOOTS a stopped distro, so recovery must never resurrect a VM the user shut down; stopped-distro state is dropped instead. Failure counters reset only after 2min of stable uptime, so connect-then-die loops escalate to the 10-min cap instead of cycling every 10s. Timer policy extracted to wsl-hook-relay-recovery.ts with direct tests. Also: version-namespaced guest install dir (dev+prod instances no longer reinstall over each other; PID-suffixed tmp files), 30s install timeout (a wedged wsl.exe could pin the state machine at 'starting' forever), per-candidate node version probing (apt node 12 on PATH no longer masks nvm node 20 into a false no-node cooldown), WSL_UTF8=1 + NUL-stripped stderr (catastrophic-failure matcher survives UTF-16LE), ordered post-sentinel chunk handoff, port-fallback breadcrumb via the home handshake, bad home reply now fails the connect, missing-bundle warn-once, case-normalized distro keys, disposeAll wired to will-quit, one-shot 60s reinstall timer for single-spawn Codex trust catch-up, escaped + contract-derived spawn command. Co-authored-by: Orca <help@stably.ai> * docs: record round-3 rig validation status for agent status over WSL (STA-1515) Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): round-4 adversarial-review fixes for the WSL hook relay (STA-1515) - dropState identity race: recovery re-checks state identity after the distro-running probe await, and the manager's dropState only deletes the exact state it was armed for — an ensure() landing mid-probe can no longer have its fresh relay orphaned outside the map. - Distro-running probe fails CLOSED: a probe error no longer reports 'running', so recovery can never wsl-d-boot a distro the user shut down. - Relay spawns use --exec: bypasses the distro's default login shell (fish/nushell chsh) and passes argv verbatim, dropping the $-escape shim; same form as the Codex WSL login spawn. - Post-sentinel chunk handoff rides a microtask so an envelope in the trailing bytes can no longer dispatch before the link's notification handler is registered. - Guest relay mirrors the SSH relay's uncaughtException/unhandledRejection posture. - Replay cache capped at 256 panes with recency eviction (the WSL relay has no per-pane teardown signal); meta map kept in lockstep. - Launch script derives the stale-exit code from the shared contract constant; one-shot reinstall timer refuses to arm after dispose. - New oracles: sentinel unit suite, fs-bridge scoping suite, fixed-token 403/204, EADDRINUSE endpoint-file rewrite, cache-cap eviction, and the recovery/manager race regressions (verified to fail with fixes reverted). - Doc: round-4 review section + revised curl.exe stance (kept as the no-node fallback — Codex is a native binary; fresh distros ship no node). * docs: record round-4 pinned rig validation for agent status over WSL (STA-1515) --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
375 lines
25 KiB
Markdown
375 lines
25 KiB
Markdown
# Agent Status over WSL (STA-1515)
|
|
|
|
Status: implemented and rig-validated (2026-07-09 round 3 + 2026-07-10 round-4 re-run
|
|
pinned to the hardened build, Windows 11 + WSL2 NAT): Claude end-to-end live —
|
|
provisioning, working→done in the store, completion toast, loopback-only posture, and
|
|
restart resume over a daemon-surviving PTY with the instance-keyed endpoint dir reused
|
|
across restarts. The round-4 re-run also proved the `--exec` spawn form live (host
|
|
process table) and the stale-exit reinstall upgrading the guest to the new bundle
|
|
version in place. Residual: Codex's done/Stop leg unproven live — env-blocked on the
|
|
rig (no dev-profile Codex credentials AND the model backend is unreachable from the
|
|
guest under NAT); everything that fired behaved correctly. Confirm on a credentialed
|
|
rig with a guest-reachable backend, ideally on a fresh distro to also observe the
|
|
deferred Codex trust entries landing after config.toml is seeded.
|
|
Owner: brennanb2025. Linear: STA-1515.
|
|
Precedent this mirrors: the SSH agent-hook relay (`src/relay/agent-hook-server.ts`,
|
|
`src/shared/agent-hook-relay.ts`, ingest at `agentHookServer.ingestRemote` in
|
|
`src/main/agent-hooks/server.ts`).
|
|
|
|
## Background — how we got here
|
|
|
|
GitHub issue `7565` reported OMP agents in WSL worktrees disappearing from the worktree
|
|
sidebar after v1.4.124. Diagnosis split it into a regression and a pre-existing class gap:
|
|
|
|
- The **regression** was a title-normalization change (PR `7447`) that stopped idle OMP
|
|
titles from producing the sidebar's title-derived fallback row. Decision: the sidebar is
|
|
moving to **hook-driven rows only** (fallback removal in flight, separate PR), so the
|
|
fallback was not restored.
|
|
- The **class gap** is that agent hooks have never worked from inside WSL for any agent.
|
|
Two scoped PRs fixed it for OMP alone (merged, live-validated on a Windows+WSL2-NAT rig):
|
|
- PR `7642` — Orca-managed WSL shells wrap interactive `omp` invocations with
|
|
`--extension "$ORCA_OMP_STATUS_EXTENSION"` (the env var is WSLENV `/p`-translated so
|
|
the WSL process reads the extension out of the Windows filesystem via `/mnt/c`).
|
|
- PR `7641` — when the extension's loopback POST cannot connect, it delivers via
|
|
Windows-side `/mnt/c/Windows/System32/curl.exe` (a Windows process, so *its*
|
|
`127.0.0.1` is the loopback Orca actually binds). Fire-and-forget spawn,
|
|
`--noproxy 127.0.0.1`, memoized WSL/curl probes, load-tolerant timeouts
|
|
(`--connect-timeout 3 --max-time 10`; 0.5s dropped events under load).
|
|
|
|
This document is the full context for the general fix: every other hook client is still
|
|
dead from WSL, and the hooks-only sidebar change makes this work the gate for the
|
|
Windows+WSL story.
|
|
|
|
## Why hooks don't work on Windows+WSL — two independent gaps
|
|
|
|
### Gap A — transport
|
|
|
|
The hook listener binds `127.0.0.1` only, deliberately (`src/main/agent-hooks/server.ts`,
|
|
`listen(0, '127.0.0.1')`; auth via `X-Orca-Agent-Hook-Token`, 403 otherwise). Every hook
|
|
client POSTs to a hardcoded `http://127.0.0.1:$ORCA_AGENT_HOOK_PORT/hook/<source>`.
|
|
|
|
WSL2 under default **NAT** networking is a VM with its own network namespace. Microsoft's
|
|
localhost forwarding is **one-way (Windows→WSL only)**: `127.0.0.1` inside WSL is WSL's
|
|
own loopback, so every POST dies `ECONNREFUSED` — silently, because hook clients are
|
|
deliberately fail-open. Reaching Windows from WSL would require the host vNIC IP (changes
|
|
per boot) + a non-loopback listener bind + a firewall rule — all three conflict with the
|
|
loopback-only security posture.
|
|
|
|
The env coordinates DO cross correctly (`src/main/pty/wsl-orca-env.ts`
|
|
`addOrcaWslInteropEnv`: WSLENV `PORT/u TOKEN/u ENV/u VERSION/u` plus
|
|
`ORCA_AGENT_HOOK_ENDPOINT/p` path-translated; called from `src/main/ipc/pty.ts` and
|
|
`src/main/daemon/pty-subprocess.ts`). The address is simply unreachable.
|
|
|
|
Opt-in **mirrored** networking (Win11, `.wslconfig`) shares loopback and makes plain fetch
|
|
work — the fix must not fight it. No `wslinfo` probing is needed: under mirrored mode the
|
|
relay's preferred-port bind collides with the Windows listener and the `EADDRINUSE`
|
|
fallback (below) handles it, while clients that go straight to the shared loopback reach
|
|
the Windows listener directly. Both delivery paths stay valid.
|
|
|
|
### Gap B — installation
|
|
|
|
Hook configs and scripts are written to the **Windows** home by every hook service:
|
|
Claude `settings.json` + managed scripts, Codex config, Gemini/Cursor/Droid/Devin/Grok/
|
|
Copilot scripts, Amp/OpenCode plugin files, the Pi/OMP extension file. An agent inside WSL
|
|
reads the **WSL-side** `$HOME` and sees none of it. There is zero WSL-targeted install
|
|
code in `src/main/agent-hooks/` or any hook service. SSH remotes have the exact precedent
|
|
needed: dedicated remote installers (`src/main/ssh/ssh-relay-session.ts` remote
|
|
settings.json handling; PR `7744` installed Droid/Copilot hooks over SSH).
|
|
|
|
Consequence: even mirrored-networking users get no hooks — transport fine, configs absent.
|
|
OMP escapes Gap B by *pointing across* the boundary (`/mnt/c` path via `/p` translation)
|
|
rather than installing WSL-side; that trick can carry file *content* for some clients, but
|
|
shell hooks still execute inside WSL and then hit Gap A regardless.
|
|
|
|
## Transport map — every client, how it posts
|
|
|
|
Endpoint file contract: `writeEndpointFile` (`src/shared/agent-hook-listener.ts`) emits
|
|
exactly four keys (`ORCA_AGENT_HOOK_PORT/TOKEN/ENV/VERSION`) to `endpoint.env` (POSIX) /
|
|
`endpoint.cmd` (Windows) — **no host field**. Shell clients source it to refresh stale
|
|
coords after an Orca restart; node clients parse it. It is never executed as a delivery
|
|
script. Clients prefer endpoint-FILE coords over env (restart re-coordination) — any
|
|
transport change must preserve that property.
|
|
|
|
| Client | Mechanism | Runtime that POSTs |
|
|
| --- | --- | --- |
|
|
| Claude, Codex, Gemini, Cursor, Droid, Devin, Grok | managed shell script | `curl` (POSIX) / `curl.exe` (Windows), built in `src/main/agent-hooks/installer-utils.ts` |
|
|
| Copilot | managed script | `curl` (POSIX) / PowerShell `Invoke-WebRequest` (Windows) |
|
|
| command-code | managed script (parse-not-source hardened) | `curl` |
|
|
| Amp, OpenCode | in-process node plugin | `fetch` |
|
|
| Pi / OMP | bundled in-process extension (`src/main/pi/agent-status-extension-source.ts`) | `fetch`, now with the WSL curl.exe fallback |
|
|
|
|
All of them target `127.0.0.1`.
|
|
|
|
## Status quo + why this gates the hooks-only sidebar
|
|
|
|
Today the worktree-card rows still have a title-derived fallback producer
|
|
(`src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts`), so WSL users
|
|
DO currently see rows for title-rich agents (Claude `✳`/spinner titles, Gemini glyphs) —
|
|
degraded (generic text, no prompt/last-message preview, no notifications) but present.
|
|
Title-poor agents are dark (Codex — hence GH `6907`). When the hooks-only change removes
|
|
that producer, **every non-OMP agent in a WSL worktree loses its card row entirely until
|
|
this work ships**. Hook fidelity adds: prompt + last-assistant-message previews,
|
|
waiting/blocked precision, completion notifications, AI Vault / native chat session
|
|
integration.
|
|
|
|
## Solution design — WSL relay + WSL-side installation
|
|
|
|
### Transport (Gap A): guest-resident relay, host-owned stdio
|
|
|
|
Run a small receiver **inside WSL on WSL's own loopback, listening on the very port the
|
|
clients were already given** (`$ORCA_AGENT_HOOK_PORT` — free inside WSL, since that port
|
|
only exists on the Windows side). Unmodified clients then deliver successfully with
|
|
**zero client changes**; the reporter's diagnostic relay in GH `7565` proved this shape
|
|
live. Forward each parsed envelope to the Windows host over the relay's **own stdio**
|
|
(Orca spawns it via `wsl.exe`, so it owns that pipe). Ingest through the existing trust
|
|
boundary: `agentHookServer.ingestRemote` (`src/main/agent-hooks/server.ts`), envelope
|
|
shape `src/shared/agent-hook-relay.ts` — identical to the SSH relay, which runs a
|
|
loopback-only receiver on the remote box and forwards over the SSH control channel.
|
|
|
|
**Port binding.** Bind the inherited `$ORCA_AGENT_HOOK_PORT` first — it keeps every
|
|
already-crossed coordinate (env and the `/p`-translated endpoint file) truthful with zero
|
|
divergence. On `EADDRINUSE` inside the guest, fall back to the SSH relay's own pattern:
|
|
bind `127.0.0.1:0`, write a **WSL-side endpoint file**, and point WSL PTYs'
|
|
`ORCA_AGENT_HOOK_ENDPOINT` at it (clients already prefer endpoint-file coords over env).
|
|
The relay writes that WSL-side endpoint file in **both** modes so restart re-coordination
|
|
never depends on `/mnt/c` translation being readable.
|
|
|
|
Lifecycle: one relay per distro **per Orca instance** (concurrent instances have distinct
|
|
ports, so guest listeners never collide); **ensure** — not just start — whenever a WSL PTY
|
|
exists: first spawn *and* daemon-PTY reattach after an Orca restart (WSL PTYs survive in
|
|
the daemon; the new instance has a new port + token and must respawn the relay before
|
|
surviving agents re-coordinate). The relay **exits when its stdin closes**: a lingering
|
|
guest listener would let WSL's own Windows→WSL forwarder grab the freed Windows-side port
|
|
and blackhole stale Windows-side hook posts. Restart if WSL restarts; token still
|
|
validated at the relay's HTTP receiver; harmless under mirrored networking (bind
|
|
fallback) and inert on non-WSL platforms (ensure only fires from WSL PTY spawns).
|
|
|
|
Reliability contract (invariant class `agent-session.hook-transport`): hook clients are
|
|
fail-open silent, so the relay must not be — spawn failures, `EADDRINUSE` fallback, and
|
|
forward errors each leave a diagnosable breadcrumb; the `wsl.exe` "Catastrophic failure
|
|
(E_UNEXPECTED)" retry is **bounded** with backoff, never a spawn loop. Oracle: provider-
|
|
contract tests with fault injection (stdin close → exit, occupied port → fallback + file
|
|
rewrite, envelope round-trip to `ingestRemote`) rather than end-to-end flows only.
|
|
|
|
Design notes from a survey of comparable WSL-capable tools (kept nameless per policy):
|
|
- Guest-resident component + host-owned channel + guest-side installation, explicitly
|
|
reusing the tool's SSH-remote machinery, **is the established pattern**. No surveyed
|
|
tool makes guest processes dial back to a Windows-localhost listener — the merged OMP
|
|
curl.exe stopgap is the outlier as a primary path (but see the round-4 revised
|
|
stance below: it survives as the no-node fallback).
|
|
- Prefer host-owned **stdio** over Windows→WSL localhost port forwarding (wslhost
|
|
forwarding is known-flaky under load; one surveyed tool dials the distro vNIC IP just to
|
|
avoid it — stdio sidesteps the question entirely).
|
|
- WSL offers no persistent control channel between separate `wsl.exe` invocations —
|
|
collapse the relay's ensure-installed + launch into **one idempotent script per spawn**.
|
|
- Install into the guest from inside the guest (download/extract in WSL) or stream the
|
|
binary through `wsl.exe` stdin — not by copying through `/mnt/c`.
|
|
|
|
### Installation (Gap B): WSL-side hook installers
|
|
|
|
Write agent hook configs/scripts into the WSL-side home, per agent, analogous to the SSH
|
|
remote installers — via `wsl.exe`-executed scripts (preferred, mirrors SSH most closely)
|
|
or `\\wsl.localhost\<distro>\...` writes. Without this half, the relay receives nothing:
|
|
the hook clients themselves are absent from the WSL filesystem.
|
|
|
|
## Alternatives considered (and why not)
|
|
|
|
1. **Endpoint-file host/URL field** — the file already crosses into WSL (`/p`-translated),
|
|
but clients read only PORT/TOKEN and hardcode the host, so all ~13 still need edits;
|
|
and a WSL-reachable listener bind breaks the loopback-only posture (LAN exposure,
|
|
firewall prompts).
|
|
2. **Replicate the curl.exe bridge per client** — the shell clients share ~2 generated
|
|
builders so it is cheaper than it sounds, but it is N point fixes, requires WSL interop
|
|
enabled (`/etc/wsl.conf` can disable it), pays a per-event process spawn (load-sensitive,
|
|
see validation facts), and keeps the ecosystem-outlier direction.
|
|
3. **Listener-side bind changes / rely on mirrored networking** — posture conflict /
|
|
opt-in only.
|
|
4. **OSC 9999 in-band status** (`src/shared/agent-status-osc.ts`, parsed per-pane in
|
|
`pty-transport.ts` and `orca-runtime.ts`) — zero-network and pane-attributed, but only
|
|
viable for in-process clients and carries status payloads, not the full hook event
|
|
vocabulary (prompts, tools, completion) — cannot replace the pipeline.
|
|
|
|
## Facts + gotchas from the 2026-07-08 Windows-rig validation
|
|
|
|
- curl.exe interop delivery works under NAT (shipped for OMP), but per-event process spawn
|
|
is load-sensitive: `--connect-timeout 0.5` dropped 3/3 events to a *healthy* listener
|
|
under load; fine at 3s. A resident relay avoids per-event spawns entirely.
|
|
- `wslinfo --networking-mode` distinguishes NAT vs mirrored.
|
|
- Clients prefer endpoint-FILE coords over env. Testing gotcha: unset
|
|
`ORCA_AGENT_HOOK_ENDPOINT` in synthetic tests or events go to the real running app.
|
|
- Server ingest silently drops paneKeys that are not `uuid:uuid`-shaped — use real-shaped
|
|
keys in synthetic validation.
|
|
- OMP is a Bun single-file binary; Bun's `node:child_process`/`fetch` compat held. Other
|
|
in-process clients run inside their agents' runtimes — verify per runtime.
|
|
- Environmental: fresh WSL 2.7.10 intermittently threw "Catastrophic failure
|
|
(E_UNEXPECTED)" from `wsl.exe -d <distro> -- bash -lc` under concurrent spawn load
|
|
(cleared by `wsl --terminate`). The relay spawn path should tolerate/retry this.
|
|
- Fork-PR CI runs sit in `action_required` until approved:
|
|
`gh api repos/stablyai/orca/actions/runs/<id>/approve -X POST`.
|
|
|
|
## Acceptance
|
|
|
|
On a default-config Windows 11 + WSL2 **NAT** machine: launch **Codex or Claude**
|
|
(explicitly not OMP) in a WSL worktree → live hook-driven worktree-card row with status
|
|
transitions and a completion notification; hook listener still bound to Windows loopback
|
|
only; zero per-client transport changes; hooks installed WSL-side automatically (no manual
|
|
config); harmless under mirrored networking and inert on non-WSL platforms. After an Orca
|
|
restart with the WSL agent still running (daemon-surviving PTY), status events resume
|
|
without relaunching the agent.
|
|
|
|
## 2026-07-09 Windows-rig validation follow-ups
|
|
|
|
The first live GUI run proved every mechanism in isolation but failed end-to-end, yielding
|
|
two fixes:
|
|
|
|
1. **Link death must be handled, not just child death.** A mux protocol error or timeout
|
|
can kill the host↔guest link while the guest process (and its 204-returning receiver)
|
|
stays alive — the exact observed signature: hooks POST 204, store never populates.
|
|
`wsl-hook-relay-link.ts` now guarantees exactly-once death handling from either signal;
|
|
the manager breadcrumbs it, kills the child, and self-restarts after a short cooldown
|
|
(a live agent session produces no new PTY spawns to re-trigger ensure).
|
|
`ORCA_WSL_HOOK_RELAY_DEBUG=1` traces every received envelope pre-ingest so a live rig
|
|
can pinpoint any residual drop. The full host chain is pinned by a live integration
|
|
test (real bundle over real child stdio through the real manager into a real
|
|
`ingestRemote`).
|
|
2. **(Round 2) The renderer's SSH-era ownership gate dropped `wsl:*` events.** With the
|
|
link fixed, envelopes reached `ingestRemote` and the durable cache, but
|
|
`useIpcEvents.applyAgentStatus` compares the stamped connectionId against the owning
|
|
repo's — `"wsl:<distro>" !== null` for a local repo, so every WSL-relayed status died
|
|
before `setAgentStatus`/notifications. Fix: `wsl:*` ids are transport provenance, not
|
|
ownership — the gate normalizes them to local (null) via
|
|
`isWslHookRelayConnectionId`, while still rejecting WSL-stamped events against
|
|
SSH-owned repos. Provenance stays stamped (it made the drop diagnosable in the first
|
|
place).
|
|
3. **Codex reads a redirected home.** Orca launches WSL Codex with `CODEX_HOME` pointed at
|
|
the managed runtime home (`~/.local/share/orca/codex-runtime-home/home`), so installing
|
|
hooks to `~/.codex` left Codex dark. The installers now accept an explicit codex home;
|
|
the trust write into `config.toml` is deferred while that file doesn't exist (the
|
|
launch path seeds it only-if-absent — creating it first would cancel the seed), and the
|
|
manager re-runs the idempotent installers on later ensures (throttled) to upsert trust
|
|
once the seed lands. Consequence: the very first WSL Codex session after a cold relay
|
|
may miss hooks; the next one has them.
|
|
|
|
## 2026-07-09 adversarial-review hardening (pre-rig round 3)
|
|
|
|
Four independent review lenses over the full diff; confirmed findings fixed:
|
|
|
|
- **Endpoint identity (all 4 reviewers)**: the guest endpoint dir was keyed by the
|
|
ephemeral Windows hook port, so a daemon-surviving agent kept sourcing the DEAD
|
|
`port-P1` file after an Orca restart — breaking the restart-resume acceptance criterion
|
|
and regressing shipped OMP recovery. Now keyed by a restart-stable instance key
|
|
(hash of the Windows endpoint file path = userData + namespace, crossed via
|
|
`ORCA_WSL_HOOK_INSTANCE`): the restarted instance's relay REWRITES the same file, which
|
|
is exactly what re-coordinates survivors.
|
|
- **Restart policy**: every failure now arms the restart timer (one failed relaunch no
|
|
longer ends self-recovery), and the timer probes `wsl --list --running` first — `wsl -d`
|
|
BOOTS a stopped distro, so recovery must never resurrect a VM the user shut down; a
|
|
stopped distro's state is dropped instead (next WSL terminal re-ensures). Failure
|
|
counters only reset after 2 min of stable uptime, so connect-then-die loops escalate to
|
|
the 10-min cap instead of cycling every 10s.
|
|
- **Install-dir versioning**: the guest install dir is namespaced by bundle version, so
|
|
concurrent Orca instances with different bundles (dev + prod) never reinstall over each
|
|
other; tmp files carry the guest PID. The install spawn also gained the 30s timeout it
|
|
was missing (a wedged wsl.exe could previously pin the state machine at 'starting'
|
|
forever).
|
|
- **Guest node resolution**: candidates (PATH, nvm glob, fixed paths) are each
|
|
version-probed, first pass wins — an apt node 12 on PATH no longer masks an nvm node 20
|
|
into a false "no node >= 18" 10-minute cooldown.
|
|
- **wsl.exe text handling**: `WSL_UTF8=1` on all spawns + NUL-stripping on stderr, so the
|
|
"Catastrophic failure" transient-retry matcher and breadcrumbs survive UTF-16LE output.
|
|
- Smaller: ordered post-sentinel chunk handoff (frame-decoder desync race), port-fallback
|
|
breadcrumb now reaches host logs via the home handshake, bad home reply fails the
|
|
connect (was: silently 'running' without installs), missing-bundle warn-once, distro
|
|
map keys case-normalized, `disposeAll` wired to app `will-quit`, single-spawn Codex
|
|
trust catch-up via a one-shot 60s reinstall timer.
|
|
|
|
Accepted gaps (reviewed, deliberately not addressed here): old version-namespaced
|
|
install dirs accrete across upgrades (~200KB each); an outdated running daemon
|
|
/p-translates the guest endpoint path until it restarts (hook scripts fall back to env
|
|
coords, which same-port binding keeps correct); `wslDistroCache` caches a transient
|
|
empty list for the app run (pre-existing semantics, now load-bearing for default-distro
|
|
resolution); default-distro resolution caches the first answer for the app run.
|
|
|
|
## 2026-07-09 round-4 external adversarial review
|
|
|
|
A second adversarial sweep (five independent lenses: guest relay + fs bridge, host
|
|
lifecycle state machine, app integration + renderer gate, design-vs-alternatives, and a
|
|
platform fact-check of every WSL claim). Design verdict: the guest-resident relay over
|
|
host-owned stdio is the right architecture — the zero-per-client-change chokepoint is
|
|
what the curl.exe alternative cannot match, and the lifecycle weight is inherent to any
|
|
guest-resident helper. Confirmed findings, all fixed on this branch:
|
|
|
|
- **`dropState` identity race (major)**: the recovery timer re-checked state identity
|
|
only BEFORE the async `wsl --list --running` probe; an ensure() landing during the
|
|
probe could get its fresh state deleted by key — orphaning a live relay child outside
|
|
the map (unkillable by `disposeAll`, duplicate relay on next ensure). Fixed: identity
|
|
re-check after the probe await + identity-guarded delete in the manager.
|
|
- **Distro-running probe failed OPEN**: any probe error (including its 10s timeout)
|
|
reported "running", so recovery could `wsl -d` — and thereby BOOT — a distro the user
|
|
shut down, in exactly the wedged-wsl.exe failure mode where the probe errors. Now
|
|
fails closed: drop the state; the next WSL PTY spawn re-ensures.
|
|
- **Spawn form hardened to `--exec`**: `wsl.exe -- <cmd>` routes through the distro's
|
|
default login shell (Microsoft docs: only `--exec` runs "without using the default
|
|
Linux shell"), so a fish/nushell chsh could mangle the launch; `--exec sh -c`/-`s`
|
|
bypasses it and passes argv verbatim (no `$`-preprocessing, escaping shim dropped) —
|
|
same form as the Codex WSL login spawn.
|
|
- **Post-sentinel handoff microtask**: pending chunks flushed synchronously inside the
|
|
mux constructor, before the manager could register notification handlers — an
|
|
envelope arriving in the trailing bytes dispatched to zero handlers (recovered only
|
|
by the later replay request). Flush now rides a microtask: after the caller's
|
|
synchronous wiring, still ahead of any subsequent stdout IO event.
|
|
- **Relay process posture**: the guest relay now mirrors the SSH relay's
|
|
`uncaughtException` (log + exit → manager respawns) / `unhandledRejection` (log +
|
|
survive) handlers.
|
|
- **Replay cache recency cap**: the WSL relay has no per-pane teardown signal, so the
|
|
per-pane replay cache grew for the relay's lifetime; now capped at 256 panes,
|
|
evicting longest-idle first (meta map kept in lockstep). Backstop for SSH too.
|
|
- Smaller: guest launch script derives the stale-exit code from the shared contract
|
|
constant (was a hardcoded 42 twin); one-shot reinstall timer refuses to arm after
|
|
dispose; fs-bridge scope comment states the lexical (symlink-following) bound
|
|
honestly. New oracles: sentinel unit suite (chunk splits, overflow kill, timeout,
|
|
microtask handoff), fs-bridge scoping suite, 403 + fallback endpoint-file rewrite,
|
|
cache-cap eviction, and the recovery/manager race regressions.
|
|
|
|
**Revised stance on the OMP curl.exe bridge — keep it, do not retire.** The relay
|
|
requires node ≥ 18 in the distro; a fresh WSL Ubuntu ships none, Codex CLI is a native
|
|
binary that brings none, and Claude Code's native installer no longer implies a system
|
|
node. A distro running only Codex would hit the no-node cooldown and stay dark — the
|
|
exact GH `6907` shape. The interop bridge is the one delivery path with no guest
|
|
runtime requirement, so it stays as the documented no-node fallback (currently wired
|
|
for OMP; extending it to the shared shell-script builders is the tracked follow-up if
|
|
no-node distros show up in telemetry). The relay remains the primary path: resident
|
|
(no per-event spawn cost) and interop-independent.
|
|
|
|
## Implementation map
|
|
|
|
- Guest: `src/relay/wsl-agent-hook-relay.ts` (entry; exits on stdin close),
|
|
`src/relay/wsl-hook-fs-bridge.ts` (home-scoped fs RPCs for installs),
|
|
`src/relay/agent-hook-server.ts` (`token`/`preferredPort` options + `EADDRINUSE`
|
|
fallback). Bundled by `config/scripts/build-relay.mjs` → `out/relay/wsl/`.
|
|
- Host: `src/main/agent-hooks/wsl-hook-relay-manager.ts` (per-distro state machine),
|
|
`wsl-hook-relay-launch.ts` (bundle resolve, guest launch/install scripts, spawn env,
|
|
sentinel wait), `wsl-hook-relay-link.ts` (envelope forward + exactly-once link-death
|
|
handling), `wsl-hook-relay-deps.ts` (DI seam), `wsl-hook-fs-adapter.ts` (SFTP-shaped
|
|
adapter + `installWslGuestHooks`, which targets Codex's managed runtime home).
|
|
- Wiring: `buildPtyHostEnv` (`src/main/ipc/pty.ts`) ensures the relay on every WSL spawn
|
|
and repoints `ORCA_AGENT_HOOK_ENDPOINT` at the guest endpoint file once known;
|
|
`src/main/pty/wsl-orca-env.ts` picks `/u` vs `/p` by value shape.
|
|
- Contract shared by both sides: `src/shared/wsl-hook-relay-contract.ts`.
|
|
- Oracles: `src/relay/wsl-agent-hook-relay.test.ts`,
|
|
`src/main/agent-hooks/wsl-hook-relay-manager.test.ts` (fault injection: stale-42
|
|
reinstall, no-node-43 cooldown, bounded E_UNEXPECTED retry, exit re-ensure gating, and a
|
|
full installer run against an in-memory guest).
|
|
|
|
## References
|
|
|
|
- GitHub: issues `6907` (Codex/WSL), `7091` + `7565` (OMP, fixed), `7563` (WSL CLI
|
|
detection, adjacent); PRs `7642` + `7641` (OMP fixes), `7744` (SSH hook installers
|
|
precedent), `7447` (title-collapse regression).
|
|
- Linear: STA-1515 (this work; ticket comments carry the same context).
|
|
- Key files: `src/main/agent-hooks/server.ts`, `src/shared/agent-hook-listener.ts`,
|
|
`src/shared/agent-hook-relay.ts`, `src/relay/agent-hook-server.ts`, `src/relay/relay.ts`,
|
|
`src/main/pty/wsl-orca-env.ts`, `src/main/agent-hooks/installer-utils.ts`,
|
|
`src/main/pi/agent-status-extension-source.ts`, `src/main/ssh/ssh-relay-session.ts`,
|
|
`src/main/providers/windows-shell-args.ts`, `src/shared/wsl-login-shell-command.ts`.
|