mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 08:02:35 +00:00
* fix(worktrees): cover WSL distro history and bound the retirement backfill scan (STA-4472, STA-4473)
* fix(worktrees): bound outstanding retirement backfill listings, not just their rate
The scan deadline abandons a listing, it cannot cancel one: an unabortable readdir
keeps its libuv threadpool thread until the OS releases it. The failure backoff paced
retries but never counted the abandoned calls, so a mount that stayed wedged stacked a
new stuck thread on every lapse until the four-thread pool starved every other
filesystem user in the process. UNC reads were already bounded by the WSL gate's
permit accounting; plain SMB/NFS listings were not.
Cap the outstanding listings process-wide and serve the memoized failure once the cap
is reached. Recovery is preserved: a slot frees as soon as an abandoned listing settles.
* fix(worktrees): keep a late retirement listing, and stop deferrals penalising healthy repos
Three review findings on the scan bound:
- A listing that landed after the 15s deadline had its result discarded. Under the WSL gate
that is the common case, not an edge one: the gate admits a single scan at a time and allows
it 60s, four times this deadline. The namespace was then left unseeded on exactly the mounts
this feature exists to cover. The answer is now kept when it arrives.
- A namespace deferred at the outstanding-listing cap never touched the wedged mount, so arming
its backoff spread one bad mount's outage to repos on healthy disks. Deferrals no longer
memoize; the next create retries as soon as a slot frees.
- The cap comment claimed it bounds threadpool starvation. It bounds this module's share only;
the WSL gate and its close lane hold threads of their own. Comment corrected.
Also stub the WSL resolver in the UNC-root unit test, which otherwise shells out to wsl.exe and
boots the developer's distro on a Windows runner.
* fix(worktrees): only count stuck retirement listings, and fence the backoff on a monotonic clock
Two more review findings on the outstanding-listing cap:
- The cap counted every in-flight listing, not just the stuck ones, so it fired on healthy
machines. Nested workspaces give each repo its own scan key, so a few first-time backfills are
routinely in flight together; the third was rejected and its create then picked a name against
an unseeded registry. Only listings that have outlived their deadline occupy a slot now, which
is what the cap was always meant to bound — healthy scans finish in milliseconds.
- The backoff fence compared wall-clock times. The WSL gate this scan runs under deliberately
avoids wall time for exactly this ('would misjudge stuckness across laptop sleep or NTP steps');
a backward step pinned a namespace in its failure memo for the size of the step. Now monotonic.
A third finding — that a late listing writing into an entry a retry has replaced loses the answer —
was investigated and refuted. An entry is only ever read back through the map, and callers hold the
promise rather than the entry, so a write to a replaced entry cannot be observed. No guard added:
the test for it passed with and without one.
* fix(worktrees): gate retirement rescans per namespace, and keep a partial answer usable
Replaces the process-wide listing budget with a per-namespace rule, and stops a refused source
throwing away the sources that did read.
- A global budget was the wrong shape: one wedged mount spends it on its own retries (lapse,
restack, lapse) and then every other namespace — including repos on healthy local disks — is
refused for the process lifetime, which is a strictly larger blast radius than the wedge it
replaced. A namespace now simply may not start a second listing while its own is still stuck,
so a bad mount costs exactly one thread and nothing else is affected.
- Rethrowing a gate refusal abandoned the whole scan at the first source. For a WSL repo the UNC
workspace root is listed first, so a stuck 9P route also discarded the plain, readable
Windows-side bucket scan that needs no distro access — worse than the behaviour before the
split. Discovery now returns what it read plus a "complete" flag; the names are used, and only
the memoization is withheld so the hole is retried.
- An I/O failure was reported as a complete empty listing, so a transient EIO on a redirected or
network home memoized "nothing is retired" for the process lifetime. Only ENOENT/ENOTDIR now
count as a complete answer.
The recovery tests now release the stalled listing rather than leaving it pending forever: a
retry while the previous call is still stuck is precisely what stacks threads.
* fix(worktrees): keep the scan retryable when a WSL distro home will not resolve
Resolving a distro home shells out to wsl.exe, which returns nothing for a stopped or slow distro
(the call has a 5s timeout) or when wsl.exe is not resolvable from the Electron process. That case
dropped the distro bucket source silently and still reported the scan complete, so the empty answer
was memoized for the whole process lifetime.
That is the STA-4472 defect re-entering through the back door: the distro is exactly where a WSL
workspace's agent history lives, so a workspace whose directory is gone leaves its only surviving
evidence unread, and the next generated create reissues that cwd. It does not self-heal either —
the scan key is derived from the probe path, which is unchanged by a failed home resolution.
An unresolved distro now marks the scan incomplete, which serves the names that were found while
leaving the hole to be retried after the backoff.
* fix(worktrees): trace a WSL repo's Windows-side workspaces into the distro too
Distro discovery keyed only on the workspace root being a UNC path, but a WSL repo can legitimately
own workspaces under C:\. computeWorkspaceRootAsync mirrors the workspace dir into the distro only
when the distro home resolves at create time; when that wsl.exe call fails it falls back to the
drive path, and those workspaces stay on the Windows side.
The agent is still spawned through wsl.exe, so its cwd is the drvfs mirror (/mnt/c/...) and its
bucket lands in the distro's own ~/.claude/projects, where the host-home scan cannot see it. The
scan then reported complete and memoized the empty answer, so the name was reissued and the next
occupant inherited the previous conversation — the STA-4472 defect, in the one configuration the
UNC check does not cover.
Which distro to look in comes from the repo path rather than the workspace root, since that is what
still identifies the distro once the root is a drive path.
* revert drvfs-mirror discovery, and pin the "no agent state" classification
Reverts the previous commit. The drvfs branch traced a WSL repo's Windows-side workspaces into the
distro, but its production wiring cannot be pinned: the distro comes from parseWslPath(repo.path),
which short-circuits off win32, so no assertion on a Linux or macOS runner can reach it — deleting
the wiring line left every test green. Shipping an unpinnable branch is the exact unreached-module
shape this PR exists to close, and it is not worth it here: the branch only pays off in a narrow
race where getWslHomeAsync fails while the workspace root is computed and then succeeds seconds
later during discovery. Whenever the distro home resolves, the root is UNC and the existing path
already covers it; whenever it does not, the scan is already reported incomplete and retried.
Also adds the missing guard on the other side of the same classification: ENOENT and ENOTDIR mean
"no agent state on this machine", which is a complete answer. That is the common case for a fresh
or Codex-only install, and misclassifying it as incomplete would turn the one-time seed into a
60s-interval rescan for the life of the process. The expression had no test; it does now.
* test(worktrees): make the retirement backoff window a real assertion
The test that claimed to cover it settled the stalled listing and re-entered in the same tick, so
outstanding cleared only in a later microtask and the no-restack rule answered first. It was a duplicate
of the test above it, and the backoff clause it was meant to pin had no coverage at all: deleting
the clause left all twelve tests in both files green, so a regression that re-probes a wedged mount
on every generated create would have shipped.
Flush the microtask so the listing is genuinely settled, then assert both directions — the memo
still serves the failure inside the window, and the same call succeeds once the window lapses.
* fix(worktrees): stop trusting a UNC ENOENT, which is what a shut-down distro looks like
Windows reports an unreachable 9P route as ENOENT, so a distro that has merely been shut down is
indistinguishable from one that never held any buckets. wsl.ts already refuses to trust a UNC
ENOENT for the same reason, probing inside the distro instead.
The classification added earlier called ENOENT a complete answer, which is right for a local home
that simply has no agent state but wrong here. After a wsl --shutdown the cached distro home still
resolves, so nothing else marked the scan incomplete: the empty result was memoized for the whole
process lifetime and every later generated create in that namespace reissued names spent inside
the distro. That is STA-4472 again, reached by a different route.
ENOENT now only means "absent" off UNC.
* fix(worktrees): tell an absent distro directory apart from an unreachable 9P route
Distrusting every UNC ENOENT fixed the shut-down-distro hole but overshot: a distro where nobody
has run Claude genuinely has no ~/.claude/projects, which is the common case for Codex-only users
and for anyone running agents from the Windows side. Those namespaces could never report a
complete answer, so the one-time seed became a full rescan every 60s for the life of the process —
each one re-spawning wsl.exe and taking the single process-wide scan slot from transcript
discovery, on a path that runs on every composer open rather than only at create.
Probe the parent instead. If it lists, the child really is absent and the answer is complete; if it
does not, the route is down and the scan stays retryable. Both directions are pinned: reverting to
either of the previous behaviours turns a test red.
* fix(worktrees): walk up to a reachable ancestor, not just one level
The reachability probe checked a single parent, which only disambiguates when ~/.claude exists but
~/.claude/projects does not. The far more common shapes have the ancestors missing too: a distro
where Claude has never run has no ~/.claude at all, and a repo with no workspaces yet has neither
the workspace root nor its parent. In both, the one-level probe also got ENOENT and called the
route unreachable, which is exactly the 60s rescan loop it was added to prevent.
Walk up until a listing succeeds, bounded so a pathological path cannot hold the scan slot. One
reachable ancestor proves the route is up, so the ENOENT below it is real absence.
The test that was supposed to guard this had ~/.claude resolving, so the real shape was never
exercised — which is why the defect shipped green. Its fixture now leaves the whole chain absent
up to the distro home, and reverting to the one-level probe turns it red.
* docs(worktrees): record what gating retirement listings costs
The shared WSL filesystem gate admits one scan task process-wide, and its stuck-task check matches
scan against scan regardless of route. So retirement discovery now queues with — and on a wedged
distro can fast-fail — native-chat transcript discovery, which the ungated readdir it replaced
never could.
Gating is still right: the gate holds the only deadline and permit accounting these UNC reads get,
and without it a hung 9P route keeps a libuv thread outright. A dedicated lane would need a third
priority, which is a change to the gate rather than to this file. Writing the trade-off down so the
next reader does not have to rediscover it.
* perf(worktrees): probe reachability with stat, and record that the gate coupling runs both ways
The ancestor probe only asks whether a directory is there, but it listed it — enumerating a WSL
home over 9P, on the composer-open path, holding the single scan permit while it did. stat answers
the same question; the gate already supports the operation.
Also corrects the trade-off note added last commit, which recorded only the direction where
retirement discovery is the victim. Because the gate stuck-check matches scan against scan
regardless of route, the reverse is now true too and is the part this PR introduces: a retirement
listing wedged on one distro can fast-fail transcript discovery on a healthy one.
* fix(worktrees): drop imports the discovery extraction left unused
The rebase onto main kept main's import block, which still pulled readdir and
homedir for discovery code this branch moved into worktree-retirement-discovery.ts.
* fix(worktrees): drop the last import the discovery extraction left unused