Skip to content

fix(desktop): retire dead pooled remotes and log main-process faults - #72808

Closed
OutThisLife wants to merge 2 commits into
mainfrom
bb/desktop-remote-residue
Closed

fix(desktop): retire dead pooled remotes and log main-process faults#72808
OutThisLife wants to merge 2 commits into
mainfrom
bb/desktop-remote-residue

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

What does this PR do?

The last two pieces of #58108 that no port has picked up. #68250 took the liveness and auth-classification work, #72740 takes the global-remote pool routing, and #67578 takes the OAuth session aggregate. These two remain.

Pooled remote backends are never retired when their host dies. A pool entry backed by a remote host has entry.process === null, so the 'exit' handler that clears a dead local backend never fires, and the renderer's 60s keepalive touch keeps the idle reaper off it. Revalidation only ever probed the primary. Nothing was left to notice the host had gone, so the pool served a dead descriptor until the app restarted and every profile bound to that host stayed broken. Pooled remotes now share the primary's liveness policy — same probe on the same revalidate tick, keyed per base URL, dropped only after the same consecutive-failure limit, so ensureBackend() rebuilds on the next call.

A main-process fault leaves no trace we can read. Electron pre-installs its own uncaughtException listener and only warns on unhandled rejections, so the app typically survives and the reason goes to stderr — discarded entirely when launched from Finder or the Start menu. It never reaches desktop.log, so it is absent from hermes debug share and the user can only describe symptoms. Both are now recorded and flushed synchronously, since a fault that does prove fatal leaves no chance for the batched async flush. Five loadURL calls were unhandled too, each able to leave a blank window with nothing to explain it; they now name the surface that failed.

The second half is diagnostics rather than a behavior fix, and it is here because it is what the current round of remote-desktop reports keeps costing us: users spend hours guessing because the bundle is silent about what actually broke.

Related Issue

Related: #58108. Completes the four defects listed in that PR's review — the other two land in #72740 and #67578.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • apps/desktop/electron/remote-liveness.ts — added revalidatePooledRemoteBackends(), reusing the existing RemoteLivenessTracker policy rather than introducing a second one.
  • apps/desktop/electron/main.ts — record entry.remoteBaseUrl when a pool entry resolves to a remote, and probe the pool on the same hermes:connection:revalidate tick as the primary.
  • apps/desktop/electron/crash-forensics.ts (new) — describeCrashReason() and installCrashForensics(), dependency-injected so the handlers are tested for real rather than by reading source.
  • apps/desktop/electron/main.ts — install the handlers, and route the five unhandled loadURL calls through a labelled helper.
  • Behavioral coverage for both: pooled probe selection, the shared failure limit, streak reset on recovery, per-host isolation between profiles, and crash-record formatting and flushing.

How to Test

  1. From apps/desktop: npx vitest run --project electron
  2. From apps/desktop: npm run typecheck
  3. Point a named profile at a per-profile remote override, open it so it enters the pool, then stop the remote host. Before this change the pool serves that descriptor until restart; after it, the profile is dropped after the standard failure streak and rebuilds when the host returns.
  4. Throw from a main-process handler and confirm the stack lands in desktop.log and survives into hermes debug share.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — N/A, Desktop-only
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Local validation on macOS, branched from c2e45b555:

  • npx vitest run --project electron: 803 passed, 2 skipped
  • npm run typecheck: passed
  • ESLint and Prettier on the changed files: passed

Original diagnosis and the main.cjs implementation of both pieces are @rod-nxtlevel's in #58108; credited in the commit trailers.

OutThisLife and others added 2 commits July 27, 2026 13:03
A pooled backend entry pointing at a remote host has no child process, so
the 'exit' handler that clears a dead local backend never fires. The
renderer's 60s keepalive touch also spares it from the idle reaper. Nothing
was left to retire the descriptor, so once the host went away the pool kept
serving it and every profile bound to that host stayed broken until restart.

Pooled remote descriptors now share the primary's liveness policy: probed on
the same revalidate tick, keyed per base URL, and dropped only after the
same consecutive-failure limit, so the next ensureBackend() rebuilds.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
Electron pre-installs its own uncaughtException listener and only warns on
unhandled rejections, so a main-process fault usually leaves the app running
with the reason on stderr — which nothing captures when the app is launched
from Finder or the Start menu. The fault never reaches desktop.log, so it is
absent from `hermes debug share` and the user can only describe symptoms.

Record both to desktop.log and flush synchronously, since a fault that does
prove fatal leaves no chance for the batched async flush. Five loadURL calls
were also unhandled, each able to leave a blank window with no explanation
anywhere the user can send us; they now name the surface that failed.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
@github-actions

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on c34ff39

all good!

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 27, 2026
@OutThisLife

Copy link
Copy Markdown
Collaborator Author

Folded into #72835 along with the rest of the cluster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants