fix(desktop): remote gateways stay connected instead of snapping back to a local backend - #93408
Merged
Merged
Conversation
…havior tests The salvaged hardening tests matched main.ts source text to assert that fetchConnectionStatus reaches for a bearer and that Apply preflights before persisting. A rename breaks them while a real auth regression that keeps the substrings passes. Make the preflight a first-class option on applyConnectionConfigAtomically so its ordering is observable, and assert it through the seam: preflight runs before either write, and a rejected preflight leaves both stores and the activation untouched.
…nce (#93197) The showAllProfiles browse-mode flag is persisted to localStorage, but every restart it was force-collapsed anyway: initializeConnectionsRegistry restores the last-used source via selectConnection, and selectConnection's post-activation path unconditionally ran $showAllProfiles.set(false). That collapse is correct for a user click on the connection picker (a concrete-source action), but the silent boot restore is not a user action. Gate both reset sites on pendingTarget === null && activeConnectionId === null (the fresh-boot state) so the persisted preference survives restart, while any user-initiated switch still collapses browse mode. Regression tests cover both directions: boot restore preserves true, a user switch collapses it. Fixes #93197
…ocal migrateV1ToRegistry runs exactly once, only when connections.json is absent. A user who was local at that moment and pointed Settings -> Gateway at a remote afterwards gets a live remote the registry cannot name: the descriptor resolves to no connectionId, primary still says 'local', and the boot-time launch pick force-switches the window onto a fresh local backend seconds after the sessions list paints. That backend has no provider, so onboarding pops. Reconcile on read: when the v1 global route names a remote with no matching registry entry, register it and adopt it as primary/last-used, then persist so the repair happens once. Narrow on purpose — an already-registered route is left alone even when primary names something else, because that is the user's pick in the Connections panel, not drift. Replaces the hand-edit-connections.json workaround users have been trading.
Reconciliation repairs the drift at its source, but it can still fail to persist (read-only or full userData), which leaves a window live on a source the registry cannot name. $activeConnectionId is null there, the preferred-id guard misses, and the restore re-homes a working connection. Return early when a connection is live but unnameable. The registry has no claim on a source it does not know about.
The toggle was gated on having 2+ registered sources, which hid it in exactly the local-only state the drift produces — the state where a user most needs to change what launch restores.
This was referenced Aug 24, 2026
Contributor
૮ >ﻌ< ა ci reviewran on 679811f — fix(desktop): show the launch-source preference for a single
|
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hermes Desktop connects to a remote gateway, paints its sessions, and then a
few seconds later re-homes itself onto a fresh local backend — greying out the
session list and popping the setup wizard, while the remote sits there healthy.
This consolidates the fixes for that and for the two other symptoms the same
drift produces.
What breaks
migrateV1ToRegistryimports the legacyconnection.jsoninto the v2connections.jsonregistry exactly once, only when the registry file does notexist. Settings → Gateway (
hermes:connection-config:apply) writes only the v1file. So anyone who was local when the registry was born and pointed Settings at
a remote afterwards ends up with two stores that disagree, permanently, with
nothing to reconcile them.
From there:
resolvedConnectionIdreturns null and$activeConnectionIdis null.initializeConnectionsRegistryrestoresregistry.primary(
local). The=== preferredIdguard misses on null, soselectConnection('local')runs andwipeSessionListsForGatewaySwitch()clears the list.
blocking overlay.
The same force-switch is what duplicates every MCP server (#91564) and what
clobbers the persisted "Show all profiles" preference (#93197). Users have been
trading a hand-edit of
connections.jsonas the workaround.The fix
Heal the drift at its source. When the v1 global route names a remote the
registry has never heard of, register it and adopt it as primary/last-used, then
persist so the repair happens once. This is what the manual JSON edit was doing.
It is deliberately narrow: an already-registered route is left alone even when
primarynames something else, because that is a real pick in the Connectionspanel, not drift.
Stop creating new drift. Apply now reconciles the v2 registry in the same
transaction as the v1 write, so
Save and reconnectpublishes one identity toboth stores. Both writes plus the re-home roll back together, and an
authenticated REST + WebSocket preflight runs before either file changes.
Authenticate liveness probes. Primary and pooled remote checks went out
anonymously, so a gateway that protects
/api/statusreturned 401 and thetracker condemned a perfectly healthy connection. They now use the same
credentials as real traffic (native bearer, then OAuth session).
Two guards behind that. The boot restore returns early when a connection is
live but unnameable — the registry has no claim on a source it does not know.
And the launch-source toggle is no longer hidden when only one connection is
registered, which was exactly the state the drift produces.
Testing
Bug-class coverage: drift heals a v1 remote into a local-only registry and the
live descriptor resolves to it; an already-registered route is untouched; a
deliberate local primary is never overridden; local/ssh/unparseable v1 routes
are ignored; other registered sources survive the heal; boot restore leaves a
live unnameable connection alone; preflight runs before either store is written
and a rejected preflight leaves both untouched.
Electron suite 1620 passed / 3 skipped, renderer suite green, all three
typecheck projects and eslint clean.
Two salvaged tests asserted against
main.tssource text (regex forensureNativeAccessToken(baseUrl), index arithmetic for preflight ordering).Replaced with behavior tests:
preflightis now a real option onapplyConnectionConfigAtomically, so its ordering is observable through theseam instead of inferred from source layout.
Credit
Root cause analysis and reproduction by @ExManubis (#90174), with independent
confirmations from @TheAirick (Arch), @greetingsyi (Windows + remote gateway,
with the log timeline pinning the regression to the 8/17 build) and @iosmand,
whose fresh-install reproduction showed this is not only stale upgrade data
and whose "delete
connections.json" recovery is what the reconciliationautomates. @Hemir95 reported the duplicated-MCP symptom (#91564) and @TVD7678
the browse-mode reset (#93197).
Co-authored-by: Alessandro Levantini 176400450+Allecpu@users.noreply.github.com
Co-authored-by: ClintonEmok 54935030+ClintonEmok@users.noreply.github.com
Supersedes #92274, #93279
Closes #90174, #91564, #93197