Repository navigation
Conversation
Crash f2521868: a wedged WSL distro cost 431 `git remote get-url` calls in one hour, 414 of them dying on the 30s deadline, peaking at 15 concurrent `wsl.exe` children — and it escalated rather than settling, because the probe *settles* on its deadline, so the in-flight coalescer drops its entry and the next poll re-issues immediately. There was no negative cache, no circuit breaker, and no concurrency bound, so "problem infinitly repeat" was literal. Add per-host admission control at `readRemoteUrl`, the one function every forge identity probe funnels through. One unanswered probe changes nothing; two drop the host to a single slot and shed the overflow; three open a breaker that backs off 30s, 60s, then 120s. Any answer — including a repo reporting no such remote — clears the whole budget, so recovery is unattended and bounded by one cap interval. State is keyed by the runtime that executes git (native, WSL distro, SSH connection generation), so a dead distro never gates a host that is answering. Transitions leave durable crash breadcrumbs; failures do not.
📝 WalkthroughWalkthroughAdded a host-scoped Git probe breaker with WSL and SSH-generation keys. The breaker limits concurrency, queues or blocks probes, tracks unavailable results, applies capped cooldowns, and records outage breadcrumbs. Shared state now supports stale-slot recovery, SSH-generation cleanup, and bounded retention. Local and SSH remote-URL probes use the breaker, and blocked probes are transient errors. Vitest coverage verifies breaker, recovery, isolation, wedged-host backoff, and breadcrumb behavior. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/git/git-host-probe-breaker.ts (1)
151-163: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winClamp a cooldown that a backward wall-clock step pushed into the future.
tryAdmitcomparesstate.blockedUntilMsagainstDate.now(). If the system clock steps backward, for example after an NTP correction or a VM suspend,blockedUntilMscan sit far beyondGIT_HOST_PROBE_MAX_COOLDOWN_MSin the future. The host then stays suppressed for the length of the step, which removes the unattended recovery this module exists to keep. Clamp the deadline to the ceiling so the trial probe still runs.🛡️ Proposed guard
function tryAdmit(state: HostProbeState): Admission { const now = Date.now() + // A backward clock step must not stretch a cooldown past its own ceiling. + if (state.blockedUntilMs > now + GIT_HOST_PROBE_MAX_COOLDOWN_MS) { + state.blockedUntilMs = now + } if (state.blockedUntilMs > now) { return 'blocked' }The assignment keeps
blockedUntilMs > 0, so the half-open trial path still applies.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 35864bb4-5402-4fec-a18b-f05361dd0729
📒 Files selected for processing (5)
src/main/git/git-host-probe-breaker.test.tssrc/main/git/git-host-probe-breaker.tssrc/main/git/remote-url-probe-wedged-host-backoff.test.tssrc/main/git/remote-url-probe.test.tssrc/main/git/remote-url-probe.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 6 remain after this review.
Adversarial review found the breaker could do the two things it exists to prevent: block a host forever, and fail probes on a host that is fine. - The cooldown is an absolute wall-clock deadline. A backwards clock step (NTP correction, VM snapshot restore, RTC fix) left every later admission blocked for the size of the jump, and recovery is success-driven, so no probe could run to earn the success that clears it. Clamp a deadline further out than the cap back to now. - The host key came from the caller's `wslDistro` hint, not from the route the runner takes. A Windows-drive worktree linked into a WSL repo runs *host* git, so its instant successes were filed under the wedged distro and reset the streak every poll — the breaker never opened. Resolve the linked-worktree route first, take the cwd's distro before the hint, and gate on win32 the way `resolveCommand` does. Keys are case-folded because wsl.exe matches distro names case-insensitively. - Queue overflow was shed as a `blocked` error, which callers classify as a host failure. On a host with a clean budget that invented an outage: >72 concurrent probes (one hosted-review lookup per visible worktree card, all sharing the `native` key) hard-failed the tail. Only a host that has actually failed gets its backlog bounded. - The serialize rung shed instead of queueing, so two timeouts were enough to fail every concurrent probe — and a shed reaches neither recorder, so the rung sustained itself. It queues now; the wait is one deadline, after which the host has answered or the breaker has opened. - Nothing bounded an in-flight slot. A probe that never settled pinned the degraded ceiling of one for the life of the process — the defect `coalesced-probe.ts` already had to fix once. Slots carry an admission stamp and are reclaimed past 180s. - `consecutiveUnavailable` never decayed, so two failures a suspend apart still counted as consecutive and pinned the host at the degraded ceiling. - Eviction walked insertion order, and an open breaker is always the *oldest* entry (a healthy host is deleted on release and re-inserted at the end), so ~64 keys of SSH reconnect churn silently erased the wedged host's cooldown. Evict the least informative entry first, and retire an SSH connection's older generations at the source so churn stops growing the map at all. - The shed message interpolated a streak of 0 into "did not answer 0 consecutive probes". It now names the reason it was shed. Retention moves to `git-host-probe-state.ts`: it is where this fails silently, and the eviction ordering only makes sense next to the forgetting rules.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/main/git/git-host-probe-breaker.test.ts (2)
165-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the WSL host key instead of hardcoding
'wsl:Ubuntu'.
gitProbeHostKeylowercases the distro, so production emitswsl:ubuntu. The suite drives the breaker with'wsl:Ubuntu', a key the derivation never produces. The behavior under test is key-agnostic, so the assertions still pass, but the blocked-message assertion at Line 318 then pins text that no real breadcrumb contains.Build the key once from
gitProbeHostKeyand reuse it.♻️ Proposed refactor
+const WSL_HOST = gitProbeHostKey({ wslDistro: 'Ubuntu' })Then replace the literals, for example:
- const outstanding = probeOnce('wsl:Ubuntu', () => held.promise) + const outstanding = probeOnce(WSL_HOST, () => held.promise)- expect(await blockedMessage('wsl:Ubuntu')).toBe( - 'Git host wsl:Ubuntu did not answer 3 consecutive probes; suppressed for ~30s.' + expect(await blockedMessage(WSL_HOST)).toBe( + `Git host ${WSL_HOST} did not answer 3 consecutive probes; suppressed for ~30s.` )Also applies to: 311-320
262-277: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAssert the concurrent outcome from the awaited promise. Replace the mutable
.thencapture withexpect(await concurrent).toBe('answered'); this keeps the assertion direct and preserves the ordering check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3c69d1b3-ba3a-44a4-9735-b4e96ea0bcf8
📒 Files selected for processing (5)
src/main/git/git-host-probe-breaker.test.tssrc/main/git/git-host-probe-breaker.tssrc/main/git/git-host-probe-state.tssrc/main/git/remote-url-probe-wedged-host-backoff.test.tssrc/main/git/remote-url-probe.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/git/remote-url-probe.ts
- src/main/git/remote-url-probe-wedged-host-backoff.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.
| /** | ||
| * Every guarded probe carries its own deadline, but nothing here can prove it, | ||
| * and at the degraded ceiling of one slot a probe that never settled would | ||
| * block its host for the life of the process — the exact defect | ||
| * `coalesced-probe.ts` already had to fix once. Reclaim a slot past the longest | ||
| * a single guarded region can legitimately run: a 5s routing probe, a 30s | ||
| * deadline, and a 30s direct-WSL fallback retry, with room to spare. | ||
| */ | ||
| export const GIT_HOST_PROBE_SLOT_STALE_MS = 180_000 | ||
|
|
||
| /** | ||
| * "Consecutive" has to mean "within one episode". Two deadline kills either | ||
| * side of a laptop suspend say nothing about the host now, and with no decay | ||
| * they would pin it at the degraded ceiling until some probe ran alone. Sized | ||
| * like the cooldown cap: past one full backoff interval a stale blip has had | ||
| * every chance to repeat. | ||
| */ | ||
| export const GIT_HOST_PROBE_STREAK_DECAY_MS = 120_000 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Shorten the production comments.
Keep each comment to the non-obvious fact that the code needs.
src/main/git/git-host-probe-breaker.ts#L59-L76: reduce the stale-slot and streak-decay rationale to brief comments.src/main/git/git-host-probe-state.ts#L1-L9: reduce the module retention rationale to a brief summary.src/main/git/git-host-probe-state.ts#L39-L44: reduce the SSH-generation cleanup rationale to a brief summary.src/main/git/git-host-probe-breaker.ts#L150-L166: reduce the admission and queue rationale to brief comments.
As per coding guidelines, “Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code.”
📍 Affects 2 files
src/main/git/git-host-probe-breaker.ts#L59-L76(this comment)src/main/git/git-host-probe-state.ts#L1-L9src/main/git/git-host-probe-state.ts#L39-L44src/main/git/git-host-probe-breaker.ts#L150-L166
Source: Coding guidelines
| function reclaimStaleSlots(state: GitProbeHostState, now: number): void { | ||
| for (const slot of state.inFlight) { | ||
| if (now - slot.admittedAtMs > GIT_HOST_PROBE_SLOT_STALE_MS) { | ||
| state.inFlight.delete(slot) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Schedule stale-slot reclamation.
reclaimStaleSlots runs only when a later caller reaches tryAdmit. If all admitted probes never settle and no later probe arrives, queued callers remain blocked and the host state never becomes idle or evictable after GIT_HOST_PROBE_SLOT_STALE_MS.
Schedule reclamation for the earliest slot expiry. Wake waiters and remove a now-forgettable state when that timer fires. Add a test with no subsequent admission.
| function recordUnavailable(hostKey: string, state: GitProbeHostState, halfOpen: boolean): void { | ||
| state.consecutiveUnavailable += 1 | ||
| state.lastUnavailableAtMs = Date.now() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Decay the streak before recording an unavailable result.
Concurrent probes can settle more than GIT_HOST_PROBE_STREAK_DECAY_MS apart. Since decay only occurs in tryAdmit, those old failures still accumulate and can serialize or open the breaker after the defined episode window.
Proposed fix
function recordUnavailable(hostKey: string, state: GitProbeHostState, halfOpen: boolean): void {
+ const now = Date.now()
+ if (
+ state.consecutiveUnavailable > 0 &&
+ now - state.lastUnavailableAtMs > GIT_HOST_PROBE_STREAK_DECAY_MS
+ ) {
+ state.consecutiveUnavailable = 0
+ }
state.consecutiveUnavailable += 1
- state.lastUnavailableAtMs = Date.now()
+ state.lastUnavailableAtMs = now📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function recordUnavailable(hostKey: string, state: GitProbeHostState, halfOpen: boolean): void { | |
| state.consecutiveUnavailable += 1 | |
| state.lastUnavailableAtMs = Date.now() | |
| function recordUnavailable(hostKey: string, state: GitProbeHostState, halfOpen: boolean): void { | |
| const now = Date.now() | |
| if ( | |
| state.consecutiveUnavailable > 0 && | |
| now - state.lastUnavailableAtMs > GIT_HOST_PROBE_STREAK_DECAY_MS | |
| ) { | |
| state.consecutiveUnavailable = 0 | |
| } | |
| state.consecutiveUnavailable += 1 | |
| state.lastUnavailableAtMs = now |
What
Crash report f2521868 (v1.4.184, Windows, WSL Ubuntu-24.04). In the hour before the user force-killed the app: 431
git remote get-urlcalls, 414 of them dying on the 30s deadline, peaking at 15 concurrentwsl.exechildren with a max span of 120s. It ran for two hours and got worse over time (44 → 68 → 125 → 73 → 59 → 32 slow calls per 10-minute bucket). The renderer stayed healthy; every git-backed panel was permanently stale. The user's own note: "session restored problem infinitly repeat".Why it never recovered
REMOTE_URL_PROBE_TIMEOUT_MSis 30s, so a wedged host makes the probe settle, not hang.runCoalescedProbeonly joins an in-flight probe, so the entry is deleted in itsfinallyandPROBE_COALESCE_STALE_MSnever trips. The next poll re-issues immediately. There was no negative cache, no circuit breaker, and nothing bounding concurrency — so the loop fed itself: more spawns → more contention → each 30s deadline stretched toward 120s → more overlap.The fix
Per-host admission control in a new
src/main/git/git-host-probe-breaker.ts, consulted fromreadRemoteUrl— the single function every forge-identity probe funnels through (remote-ref-probe-cache,github-repository-identity,gitlab-project-ref-resolution,hosted-review), covering native, WSL and SSH/relay in one place. The git call itself is unchanged.A three-rung ladder on consecutive transient failures. A stable answer — including a repo reporting no such remote — is proof the host is alive and clears the whole budget. "Consecutive" means within one episode: a streak decays after 120s, so two failures either side of a laptop suspend are not counted together.
Backoff:
30s × 2^(opens−1), capped at 120s. 30s is one probe deadline — retrying sooner spends more wall time inside the host than outside it. 120s is the longest span a single wedged call was observed to hold the host in this report; past that, waiting buys no further idle time and only delays recovery. Neither is a permanent give-up.Host scoping (never global):
native|wsl:<distro>|ssh:<connectionId>:<generation>. The key is derived from the route the runner will actually take, not from the caller's hint: a Windows-drive worktree linked into a WSL repo runs host git and is keyednative; a\\wsl$\…UNC cwd names its distro and wins over the hint, the wayresolveCommandreads it; off Windows nothing routes into WSL. Distro names are case-folded becausewsl.exematches them case-insensitively. An SSH reconnect bumps the generation, which is a new key, so a fresh transport never serves the old one's cooldown, and the older generations of that connection are retired so churn cannot grow the state map.Recovery is unattended and needs no restart: one answer resets streak, open count and deadline. Worst case is one cap interval.
Visibility. The user in this report had no idea why every panel was stale. Transitions leave durable crash breadcrumbs —
git_host_probe_breaker_open,git_host_probe_breaker_still_open(coalesced on a 5-minute floor so a two-hour outage is still legible in a 30-entry ring),git_host_probe_recovered. Individual failures log nothing.Fix evidence
Simulated incident —
remote-url-probe-wedged-host-backoff.test.tsA wedged distro, every spawn burning its full 30s deadline, polled every 10s for a simulated hour:
The escalating shape is gone; it converges to one deadline per 150s and stays there. A single success then restores full speed — five polls afterward issue five spawns.
A 15-wide burst (the report's peak) against an already-failing host issues zero spawns. After recovery, the same 15-wide burst issues all 15 again.
Real
wsl.exeon Windows 11, Ubuntu-24.04 (the report's distro)A 15-wide burst — wider than the ceiling of 8 — is within noise of unguarded, three rounds each way:
wsl.exespawn cost already serializes these harder than the ceiling does. A genuinely cold distro answers in 1.3s, 22× inside the deadline, and leaves no failure state — which is precisely why rung 1 must be free.A nonexistent distro fails in ~35ms with a non-transient error, so the breaker correctly stays out of it (
state: null): a fast, cheap, deterministic failure is not a storm, and backing it off would only delay a distro that is about to come online.Gates
Re-run on macOS after the adversarial-review fixes:
The earlier Windows run of this branch reported
8 failed | 151 passedinsrc/main/git; those 32 failures were real-git and symlink-privilege environment failures on that machine (Command failed: git init,EPERM: symlink) and did not reproduce on macOS or in CI. All 60 GitHub Actions checks on this PR are green.ELI5
Orca keeps asking each of your repos "who hosts you?" by running a tiny
gitcommand. On Windows that means startingwsl.exe. If WSL gets stuck, that command doesn't fail — it just sits there for 30 seconds and then gets killed.The old code treated that like any other finished answer: it threw it away and asked again. And again. Fifteen questions at a time, none of them ever answered, for two hours — each one making the next one slower, until the user killed the app.
Now Orca notices. One bad answer is forgiven; WSL is genuinely slow when it wakes up. Two, and it stops asking fifteen questions at once and asks one. Three, and it stops asking for half a minute, then a minute, then settles at asking once every two minutes — forever, but gently. The moment WSL replies even once, everything snaps back to normal instantly. Nobody has to restart anything, and if it ever happens again the crash report says so in plain words.
Trade-offs
isTransientGitProbeErrorrecognises them, which is what keepshosted-reviewon its failure path and the ref caches from publishing a false negative.readRemoteUrl. A wedged host still costs each of those one deadline. Gating them would mean shedding user-initiated git, which is a different and much riskier decision.readRemoteUrlis where the report's storm actually lived.Limitations
Written to be read as a list of things this does not do.
Scope. The breaker guards
readRemoteUrlonly. Every other git call against the same wedged host — status polling, upstream checks, history — still pays a full deadline each, unbounded and uncoalesced. The incident's totalwsl.exepressure is reduced, not eliminated, and a user on a wedged distro can still see a slowdown after this fix.Recovery depends on callers. Nothing inside the module schedules a retry. Recovery works only because every consumer treats a transient/blocked probe as uncacheable (
remote-ref-probe-cachereturns null without publishing,github-repository-identitycaches only a stable missing remote,hosted-reviewthrows to stay on its failure path), so the next poll re-enters and becomes the half-open trial. If a future caller ever starts caching a transient result, recovery stalls silently: there is no watchdog and no timer in the breaker itself.A half-wedged host is not defended. A host that alternates failure and success never accumulates a streak of three, so a 50%-wedged host keeps storming at full width. That is the deliberate design — a stable answer is proof the host is alive — but it is a real gap.
TRANSIENT_PROBE_PATTERNSis also narrow: a WSL failure whose text is not literallytimed out/ETIMEDOUT/ECONNRESET/EPIPE/connection …counts as an answer and clears the entire budget.The
nativebucket covers every local repo. One repo on a dead network mount or an unmounted external drive producestimed outerrors filed undernativeand, at three consecutive occurrences, backs off the user's healthy local repos too. In practice interleaved successes keep the streak from accumulating, and the streak now decays after 120s, but nothing in the design prevents it.One guarded probe can hold a degraded host longer than one deadline.
gitExecFileAsyncmay spend up to 5s inprepareWslLinkedWorktreeGitRoutingand then retry the direct-WSL fallback with a second full timeout, so ~65s inside a single guarded region is possible. Everything else on that host is serialized behind it for that span. The 180s slot-reclamation bound is sized above this, which also means a genuinely hung probe is tolerated for up to 180s before its slot is returned.Queue fairness is unspecified.
releasewakes all waiters at once andtryAdmitadmits fresh arrivals without consultingqueued, so a queued caller can lose repeatedly to newcomers under sustained load, and there is no per-caller deadline on the wait — a queued caller inherits the wall time of the probes ahead of it. No deadlock is possible (waiters are only enqueued while a slot is occupied, and every in-flight probe releases and wakes them), but a wave of many slow-but-succeeding probes on one key could push a queued caller pastPROBE_COALESCE_STALE_MS(60s), at which point the coalescer above abandons and re-issues while the abandoned probe still holds a slot. I could not construct a realistic workload that reaches this; it is unverified residual risk, not a demonstrated defect.Wake-up cost is O(waiters) per release. With the healthy queue now unbounded, a very wide burst re-wakes every waiter on each release. At a few hundred waiters this is microseconds of closure calls; it is not free, and it is the price of never converting a healthy host's overflow into a failure.
Clock handling is a clamp, not a monotonic source. Cooldowns still use
Date.now(). A backwards step is now repaired on read (a deadline further out than the 120s cap is pulled back to now), but a forward step simply expires a cooldown early, and the module has noperformance.now()baseline.Process-global state.
gitProbeHostStatesis module-level. Only the probe suites call_resetGitHostProbeBreaker, so a future test elsewhere that produces three consecutive transient-shaped failures could gate later tests in the same file with no obvious signal.Not re-verified by this review. The Windows
wsl.exemeasurements in the evidence section above (HEALTHY_SERIAL_MS, the BURST15 rounds, the 1331ms cold start, the ~35ms nonexistent-distro failure) and the claim that the 32 failures in the original Windows run were pre-existing were taken on the author's machine and could not be reproduced during review, which ran on macOS. Everything under "Gates" was re-run.Eviction still has a floor. If all 64 tracked hosts are simultaneously mid-outage, the third eviction pass drops one that is still cooling down — bounded memory has to win somewhere. That host is then re-probed at full width once. In the reported configuration this needs 64 concurrently-wedged distinct hosts.
Adversarial review
Three independent reviewers audited this branch — none of them the author. Lenses: field-report fidelity (does the fix actually change the reported failure), self-inflicted denial of service (can the guard strand a host that is fine), state retention and eviction, non-monotonic time, concurrency and lost wake-ups, and evidence honesty (does the diagnostic text match what happened). They returned ten blocking findings (three of which are the same queue-overflow defect, found independently by all three). I rejected none: every one describes a real defect, and all ten are fixed in
27fdeba, each with a regression test that I verified fails when the corresponding guard is disabled. One sub-claim inside one finding is wrong and is called out below; the finding it sits in is still real.The notable ones:
blockedUntilMsis an absoluteDate.now()deadline with no upper clamp. An NTP step correction, VM snapshot restore or dual-boot RTC fix left every later admission on theblockedbranch for the length of the jump — hours or days, not the documented 120s — and because recovery is entirely success-driven, no probe could run to produce the success that clears it. Exactly the outcome the PR exists to prevent. Fixed by repairing a deadline further out than the cap on read.wslDistro/ the UNC path, butgitExecFileAsyncroutes a Windows-drive worktree linked into a WSL repo to host git. Those instant successes were filed underwsl:<distro>and reset the wedged distro's streak on every poll, so on a mixed layout the breaker never opened at all. Reproduced against the realreadRemoteUrlbefore the fix: 20 interleaved poll cycles, 20 spawns on the wedged host, breaker state stillnull. The PR body previously claimed this case was only a mislabelled breadcrumb; that was wrong and the text is corrected above. The key is now derived from the same routing decision the runner makes. The reviewer also claimed the inverse case (a\\wsl$UNC path with nowslDistro) runs native git — that part is incorrect:resolveCommandroutes on the cwd regardless of the hint, so a UNC cwd does execute in the distro. The only real defect in that direction was the missing platform gate, which is fixed.MAX_QUEUED_PER_HOST = 64on top of a ceiling of 8 meant caller 73+ received aGitHostProbeBlockedErrorthatisTransientGitProbeErrorreports as unavailable, even withconsecutiveUnavailable === 0. Reachable:hosted-reviewissues an uncoalescedassertRemoteUrlReadableper branch, the per-card refresh runs on mount and on everyvisibilitychange, and every local repo shares the singlenativekey. Now only a host that has actually failed has its backlog bounded.recordAnswerednorrecordUnavailable, so the rung sustained itself while one slow probe held the single slot. Downstream,hosted-review-branch-cacheturns that into a 60s → 15min lookup backoff. The overflow queues now.Map, so a host with retained failure state is always the oldest entry — andevictIdleHostStateswalked insertion order. A reviewer reproduced the wedged distro being evicted at flap 63 of SSH reconnect churn, after which the still-wedged host was probed again immediately. Eviction now drops the least informative entry first, and an SSH connection's retired generations are pruned at the source.coalesced-probe.tswas written to fix, reintroduced. Neither reviewer could construct a live hang (both call paths carry a deadline), so this is defence in depth rather than a demonstrated bug, but the guard is what keeps any future hang from turning into "this host is dead forever".consecutiveUnavailablenever decayed, so two failures a laptop-suspend apart still counted as consecutive and pinned the host at the degraded ceiling. It now decays after 120s.Also fixed while in there, from the reviewers' non-blocking notes:
parseWslUncPathhad no platform guard (a literal//wsl$/xdirectory off Windows was keyed as a distro), and host keys were not case-folded, so one distro spelled two ways split into two half-blind budgets.Retention moved into a new
git-host-probe-state.ts— it is where this fails silently, and the eviction ordering only makes sense next to the forgetting rules. Nomax-linessuppression was added or bumped.Relation to the #15039–#15257 stack
Complementary, not overlapping. Based on
main, not stacked — #15039 is already merged, and #15060 / #15257 touchrunner.ts,wsl-git-read-environment.tsandwsl-direct-git-read-commands.ts. This PR touchesremote-url-probe.tsand adds two new files. No textual conflict, and neither branch depends on the other.That stack fixes what a git call returns (argv mangling through
--exec, the login-shell banner landing on stdout, and #15257 removing the shell from reads entirely, which should cut per-call cost materially). This PR fixes how often Orca is willing to make a call that never returns. A perfectly correct, shell-free probe against a wedged or absent distro still burns its full deadline, and before this change Orca would retry that forever at unbounded concurrency.Incidental confirmation from the real-WSL run above: on stock Ubuntu 24.04 the probe came back as
— a live reproduction of exactly the banner contamination #15060/#15257 fix, observed on
mainwhile validating this change. Both fixes are needed: that one makes the answer correct, this one stops the retry storm when there is no answer at all.