Skip to content

fix(desktop): surface profile-switch failures instead of falling back to primary socket - #81165

Open
echoes666 wants to merge 3 commits into
NousResearch:mainfrom
echoes666:fix/profile-switch-v2
Open

fix(desktop): surface profile-switch failures instead of falling back to primary socket#81165
echoes666 wants to merge 3 commits into
NousResearch:mainfrom
echoes666:fix/profile-switch-v2

Conversation

@echoes666

Copy link
Copy Markdown

Fixes #81094

Problem

When switching to a secondary profile, openSecondary / ensureGatewayForProfile could silently fall back to the primary socket if the target backend's WebSocket failed to open (e.g. a manually started gateway process holding the profile's resources). This routed the user's messages to the wrong profile's backend and caused cross-profile session writes.

Changes

  • apps/desktop/src/store/gateway.ts (openSecondary): rethrow connect failures with an actionable error message instead of letting the caller's catch path fall through to the primary socket.
  • apps/desktop/src/store/profile.ts (ensureGatewayProfile): log and rethrow the switch failure instead of silently resetting the swap target.

Why fail loudly instead of retrying?

A silent fallback is strictly worse than an explicit error here: the user's message would be persisted into the wrong profile's session. Failing loudly surfaces the conflict (e.g. a manually started gateway holding the profile) and the message suggests how to resolve it. Retrying against a manually held socket would loop forever.

Notes

Replaces the previous PR #81099 (same fix, rebased onto current main; the old branch was force-pushed during a botched amend and GitHub does not allow reopening it).

… to primary socket

Fixes NousResearch#81094

When switching to a secondary profile, openSecondary / ensureGatewayForProfile could silently fall back to the primary socket if the target backend's WebSocket failed to open (e.g. a manually started gateway process holding the profile's resources). This routed the user's messages to the wrong profile's backend and caused cross-profile session writes.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) area/profiles Multi-profile isolation, HERMES_HOME scoping sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
@spfcraze

spfcraze commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
openSecondary's new rethrow is consumed by the unchanged catch { scheduleReconnect(entry) } at apps/desktop/src/store/gateway.ts:296, so a failed profile switch still ends at setActive(key) with a closed socket and the error never reaches the new catch in ensureGatewayProfile.

Problems:

  • ensureGatewayForProfile wraps await openSecondary(entry) in a bare catch { scheduleReconnect(entry) } (apps/desktop/src/store/gateway.ts:295-297) that this diff does not touch — the rethrown Failed to connect to profile ... error is discarded there, and execution continues to setActive(key) (gateway.ts:301) with the secondary socket still closed.
  • The new catch (error) in ensureGatewayProfile (apps/desktop/src/store/profile.ts) fires only if ensureGatewayForProfile or syncConnectionToActiveProfile rejects; on the connect-failure path the PR description names, ensureGatewayForProfile resolves normally, so the switch proceeds exactly as on main and the new error message is not shown. A gatewaySwitch rejection already propagated through main's finally-only try, so the catch's only new effect is the console.error.

Solution:
Make ensureGatewayForProfile propagate the failure — rethrow from its catch (or record the failure and skip setActive(key)) — so the error message built in openSecondary reaches a caller that can surface it; as written, every call site of openSecondary (reconnectSecondary, openGatewayForProfile, ensureGatewayForProfile) discards the error.


Checked against d399455 — the tip of fix/profile-switch-v2 when this was written — and b3aa561, main at the same moment.

…rough to setActive

The rethrow added in openSecondary (NousResearch#81094) was consumed by the unchanged
`catch { scheduleReconnect(entry) }` in ensureGatewayForProfile, so a
failed profile switch still ended at setActive(key) with a closed socket
and the error never reached ensureGatewayProfile's catch — the fix did not
actually surface the failure (spotted by AI triage on NousResearch#81165).

Now the catch keeps the reconnect schedule (transient failures still
self-heal) but re-throws, so the caller surfaces the error and skips
activating a profile whose backend is unreachable.
@echoes666

Copy link
Copy Markdown
Author

Thanks for catching this — you're right, the rethrow was swallowed by the unchanged catch { scheduleReconnect(entry) } in ensureGatewayForProfile, so a failed switch still fell through to setActive(key) with a closed socket and the new catch in ensureGatewayProfile never fired. The original fix was ineffective.

Fixed in 0882362 (pushed to this branch): the catch now keeps the reconnect schedule (transient failures still self-heal via the existing backoff) but re-throws, so ensureGatewayForProfile aborts before setActive, and ensureGatewayProfile receives the error and surfaces it instead of silently activating a profile whose backend is unreachable.

Verified: tsc --noEmit clean; profile.test.ts + gateway-switch.test.ts (12 tests) pass.

…file

Regression tests for the NousResearch#81094 rethrow: a failed secondary connect must
propagate (not fall through to setActive with a closed socket) while still
arming the reconnect backoff, and a successful connect activates the
entry. Mocks @/hermes, @hermes/shared and window.hermesDesktop; module
singletons are reset per test via closeSecondaryGateways.
@teknium1

Copy link
Copy Markdown
Contributor

Partial overlap with #87600, now on main: the silent-misroute half is fixed (activeGateway() returns null for missing named scopes; eviction paths restore the primary explicitly). What SURVIVES here and remains wanted: the error-surfacing UX — main's ensureGatewayForProfile still does catch { scheduleReconnect(entry) } then setActive, silently activating a scope whose socket is dead; no error reaches the user. Your revised rethrow+surface commit addresses exactly that gap. A rebase onto the post-#87600 store would slim this PR down to the UX half — happy to review it in that shape.

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/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: switching to a secondary profile can route messages to the primary backend when a manual gateway process exists

4 participants