Skip to content

fix(desktop): ignore stale backend exits - #38292

Closed
sweetcornna wants to merge 1 commit into
NousResearch:mainfrom
sweetcornna:codex/fix-38266-desktop-remote-reconnect
Closed

fix(desktop): ignore stale backend exits#38292
sweetcornna wants to merge 1 commit into
NousResearch:mainfrom
sweetcornna:codex/fix-38266-desktop-remote-reconnect

Conversation

@sweetcornna

@sweetcornna sweetcornna commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • guard desktop backend connection state with a generation/process owner
  • ignore stale local backend error/exit events after a reconnect switches to a newer connection
  • add a node:test regression for stale local exits preserving the newer connection promise

Root cause

resetHermesConnection() cleared the global connection promise and killed the old local backend, but the old process could emit exit after a newer remote connection had already started. That stale handler cleared the new connection state and broadcast a backend exit, which could push Desktop back into the boot path and amplify into repeated local spawns/port conflicts.

Validation

  • node --test electron/backend-connection-state.test.cjs
  • npm run test:desktop:platforms
  • node --check electron/main.cjs; node --check electron/backend-connection-state.cjs; node --check electron/backend-connection-state.test.cjs
  • git diff --check

Note: local npm run type-check could not complete in this checkout because tsc is not installed on PATH; CI is running the full workflow on the rebased head.

Closes #38266

@sweetcornna
sweetcornna requested a review from a team June 3, 2026 15:14
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have labels Jun 3, 2026
@sweetcornna
sweetcornna force-pushed the codex/fix-38266-desktop-remote-reconnect branch 2 times, most recently from 246476b to abbdf50 Compare June 5, 2026 07:35

Copy link
Copy Markdown
Contributor Author

Rebased this PR onto current main and resolved the Desktop conflicts in electron/main.cjs and package.json.

Conflict resolution kept the newer main changes for profile backend pooling, WebSocket connection probing, and the expanded desktop platform test set, while preserving this PR's generation/process-owner guard for the primary backend connection state. The update reaper now excludes both the primary backend from backendConnectionState and pooled profile backends.

Fresh validation on the rebased head abbdf50f5:

npm run test:desktop:platforms
# 71 passed

node --test electron/backend-connection-state.test.cjs
# 3 passed

node --check electron/main.cjs; node --check electron/backend-connection-state.cjs; node --check electron/backend-connection-state.test.cjs
# passed

npx eslint electron/main.cjs electron/backend-connection-state.cjs electron/backend-connection-state.test.cjs
# passed

npm run type-check
# passed

git merge-tree --write-tree upstream/main HEAD
# clean

git diff --check
# passed; Windows CRLF working-copy warning only

GitHub now reports the PR as mergeable.

austinpickett
austinpickett previously approved these changes Jun 9, 2026

@austinpickett austinpickett left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hermes Agent Review — ✅ Approve

Verified locally vs origin/main; ran the new test suite (3/3 pass). Paid special attention to the correctness of the stale-exit heuristic, which is the crux.

The stale-exit detection is sound. The PR extracts primary-backend lifecycle into backend-connection-state.cjs using a generation counter + per-process/per-attempt ownership tokens:

  • clearForCurrentProcess(owner) returns true only if BOTH owner.generation === generation AND owner.process === process. attachProcess() captures {generation, process} at spawn; resetHermesConnection()invalidate() bumps generation and nulls process. So a late exit/error from a superseded backend fails both checks → returns false → the new guard (first statement in both the error and exit handlers) bails out without nulling the live connection. That's exactly the targeted bug.
  • The double guard (generation + process identity) is belt-and-suspenders: generation alone suffices, but the extra identity check harmlessly defends future code paths that might swap process without bumping generation. No false positives — a genuine crash of the current backend matches both and clears normally (verified by test 2).
  • The promise-attempt path (clearPromiseForAttempt(attempt), identity check attempt.promise !== promise) guards the .catch() rejection so a stale attempt can't wipe the new attempt's connectionPromise or fire a misleading backend.error progress update (test 3).

I confirmed the main.cjs hunks are a faithful mechanical translation of the old module-level hermesProcess/connectionPromise globals into the new accessors (resetHermesConnection, teardownPrimaryBackendAndWait, startHermes single-flight via getPromise(), the bootstrap-reset IPC clearPromise(), before-quit).

node --test apps/desktop/electron/backend-connection-state.test.cjs  =>  3 pass, 0 fail

Tests exercise the real supersede race (attach → invalidate → new attempt → assert stale clear rejected and live promise survives), not smoke checks. Registered in test:desktop:platforms. No out-of-scope hunks, no new I/O/env/spawn surface (pure in-memory state machine).

Non-blocking observations (not defects): the error handler doesn't re-check !backendReady before rejecting in the stale branch (harmless — rejectBackendStart?. is a no-op once settled); clearPromise() nulls the latched promise without bumping generation, which is intentional for the failure-recovery reset path.

Cluster note: same desktop module-extraction family as #37471/#39554/#38589/etc. — textual conflicts expected, no semantic overlap.

Reviewed by Hermes Agent (local node --test + origin/main verification).

@sweetcornna
sweetcornna force-pushed the codex/fix-38266-desktop-remote-reconnect branch from 1d210d3 to 7ea439f Compare June 11, 2026 08:23
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Desktop lifecycle work. This is an automated hermes-sweeper review: the reported reconnect ordering is already implemented on current main.

  • apps/desktop/electron/main.ts:7714-7715 awaits primary-backend teardown before notifying the renderer to reconnect.
  • apps/desktop/electron/main.ts:6393-6421 waits for the prior child’s exit event, preventing the old local child from overlapping the replacement connection.
  • apps/desktop/electron/main.ts:6762-6785 returns a healthy remote connection before the local-start branch at :6787.
  • This behavior was introduced by 6feb40e702829639c22aae2ae29d63e9ac5e8508 (fix(desktop): wait for backend exit before reloading on connection-config apply).

The PR’s main.cjs implementation was subsequently superseded by the TypeScript conversion (39d09453f95e8aefc0c97e5d9b30ff341cae9ed8).

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

Labels

P3 Low — cosmetic, nice to have sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Hermes Desktop 0.15.1 remote gateway enters reconnect loop after successful connection

4 participants