Skip to content

fix(desktop): multi-profile message routing sends messages to wrong profile (#67097) - #67254

Closed
webtecnica wants to merge 1 commit into
NousResearch:mainfrom
webtecnica:fix/67097-desktop-profile-routing
Closed

fix(desktop): multi-profile message routing sends messages to wrong profile (#67097)#67254
webtecnica wants to merge 1 commit into
NousResearch:mainfrom
webtecnica:fix/67097-desktop-profile-routing

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Summary

Fixes #67097 — Desktop multi-profile message routing intermittently sends messages to the wrong profile's gateway.

Root Causes & Fixes

1. activeGateway() silent fallback to primary gateway (store/gateway.ts)

Problem: When activeKey pointed to a non-primary profile and the secondary didn't exist (transient state), activeGateway() silently fell back to primaryGateway, routing messages meant for a remote profile to the local backend.
Fix: Return null instead of primaryGateway — the callers already handle null.

2. Secondary WebSockets not closed on primary switch (store/gateway.ts)

Problem: ensureGatewayForProfile("default") called setActive(key) and returned immediately for the primary profile, but left all secondary WebSocket connections open with wantOpen = true. Stale connections could receive messages if active key state was confused.
Fix: Call closeSecondaryGateways() before setActive() when switching to the primary profile.

3. HERMES_SESSION_PROFILE env var empty (electron/main.ts)

Problem: Both startHermes() and spawnPoolBackend() inherited HERMES_SESSION_PROFILE="" from process.env. Subprocess tools that query get_session_env("HERMES_SESSION_PROFILE") got an empty string instead of the correct profile name.
Fix: Set HERMES_SESSION_PROFILE explicitly:

  • startHermes(): activeProfile || 'default'
  • spawnPoolBackend(): the profile name

4. active-profile.json never created on boot (electron/main.ts)

Problem: active-profile.json was only written when the user explicitly switched profiles via the UI. On first boot (missing file), there was no persisted record of the profile state.
Fix: Initialize active-profile.json in app.whenReady() by calling writeActiveDesktopProfile(null) when the file is missing.

Files Changed

  • apps/desktop/src/store/gateway.ts — Fixes 1 & 2
  • apps/desktop/electron/main.ts — Fixes 3 & 4

…rofile

Fix NousResearch#67097 — four root causes addressed:

1. store/gateway.ts: activeGateway() no longer silently falls back to
   primaryGateway when the active profile's secondary doesn't exist.
   Returns null instead, preventing messages meant for a remote/secondary
   profile from routing to the local primary backend.

2. store/gateway.ts: ensureGatewayForProfile() now closes all secondary
   WebSocket connections before activating the primary profile. Stale
   secondary connections with wantOpen=true could receive messages if
   active key state was confused.

3. electron/main.ts: Set HERMES_SESSION_PROFILE in spawn environment for
   both startHermes() (primary backend: activeProfile || 'default') and
   spawnPoolBackend() (pool: profile). Previously the env var was empty,
   causing subprocess tools to query an empty string instead of the
   correct profile name.

4. electron/main.ts: Create active-profile.json on first boot in
   app.whenReady(). Previously the file was only written on explicit
   profile switch via UI, leaving no persisted state on fresh install.
@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 19, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #67097 and open #67190. This PR includes #67190's activeGateway fallback repair but also adds independent profile persistence, subprocess profile propagation, and secondary-socket cleanup; it is a competing superset rather than a duplicate.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tracing the fallback route; that part addresses a real current-main defect in apps/desktop/src/store/gateway.ts:74.

Problems

  • closeSecondaryGateways() in the primary-profile path would tear down live background streams. Current main explicitly says background sockets are never closed in this path (apps/desktop/src/store/gateway.ts:205-236), and profile switching relies on keeping them alive for concurrent streaming (apps/desktop/src/store/profile.ts:253-295).
  • The new HERMES_SESSION_PROFILE spawn value is not the per-turn source of truth: gateway turns bind it from context.source.profile (gateway/run.py:15558-15569), and get_session_env() prefers that ContextVar over process environment (gateway/session_context.py:304-327).
  • The boot write persists { "profile": null }, which readActiveDesktopProfile() reads as the same null value as a missing file (apps/desktop/electron/main.ts:5863-5892).
  • Please add a regression test for the non-primary active-key/absent-secondary case; #67190 demonstrates the focused test shape.

Suggested changes

  • Keep the null fallback, remove the unconditional secondary teardown, and re-scope the profile propagation/persistence additions to verified behavior.

Automated hermes-sweeper review.

if (key === primaryProfile) {
// Close all secondary WebSocket connections before activating the primary.
// Leaving secondaries open with wantOpen=true while the active key flips to
// primary creates stale connections that can receive messages if the active

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This closes every live background-profile socket when returning to the primary profile, contradicting the current concurrent-streaming contract: ensureGatewayForProfile() intentionally preserves background sockets (apps/desktop/src/store/gateway.ts:205-206) and ensureGatewayProfile() relies on them staying live (apps/desktop/src/store/profile.ts:253-295). Please remove this call from the primary fast path.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 19, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

One PR, #67254, addresses issue #67097. Its diff fixes the demonstrated wrong-gateway fallback, while the contributor review identifies design or effectiveness problems with the secondary-socket cleanup, spawn-time profile propagation, and null-profile boot write.

Related pull requests

Duplicates

#67254 overlaps open #67190 on the activeGateway() fallback repair, but the contributor discussion characterizes #67254 as a broader competing superset rather than a full duplicate.

Suggested consolidation

Keep #67254 open with a salvage path: preserve the null-return fallback fix and add the focused regression test, while explicitly resolving the contributor review's lifecycle, session-context, and persistence objections. Do not close it as a duplicate because its diff extends beyond #67190, and no listed PR can be closed as a duplicate on the supplied evidence.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I67097(["issue #67097 (open)"])
    P67254["PR #67254 (open)"]
    P67254 -->|best fix| I67097
    class I67097 open
    class P67254 open
    class P67254 best
    class P67254 target
    click I67097 "https://github.com/NousResearch/hermes-agent/issues/67097"
    click P67254 "https://github.com/NousResearch/hermes-agent/pull/67254"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 4 kB of PR diffs, 5 kB of issue/PR text, 5 kB of discussion (5 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@teknium1

Copy link
Copy Markdown
Contributor

The core fix here (activeGateway's silent primary fallback) landed on main via #87600, which also syncs $connection/$activeGatewayProfile on every activation and serializes switches through a shared mutex. Closing as implemented on main — thanks for the analysis and the PR.

@teknium1 teknium1 closed this Aug 16, 2026
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/*) needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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: multi-profile message routing sends messages to wrong profile's gateway

4 participants