Skip to content

fix(desktop): await gateway swap in selectProfile before requesting fresh session - #39837

Closed
pcaruba wants to merge 1 commit into
NousResearch:mainfrom
pcaruba:fix/select-profile-race-condition
Closed

pcaruba wants to merge 1 commit into
NousResearch:mainfrom
pcaruba:fix/select-profile-race-condition

Conversation

@pcaruba

@pcaruba pcaruba commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Problem

When clicking a profile square in the desktop sidebar's ProfileRail, the profile pill highlights correctly but the chat continues talking to the old profile's agent. The user sees the new profile selected in the UI but their messages still go to the previous profile's backend.

Root cause

selectProfile() in store/profile.ts calls ensureGatewayProfile(target) with void (fire-and-forget), then immediately calls requestFreshSession() to navigate to a new chat. This creates a race condition:

  1. void ensureGatewayProfile(target) starts the async gateway swap (spawning a pooled backend, establishing WebSocket)
  2. requestFreshSession() runs synchronously — clears state, navigates to NEW_CHAT_ROUTE, shows a fresh draft
  3. The gateway swap is still in flight — the secondary socket to the target profile's backend isn't connected yet
  4. User types a message → createBackendSessionForSend does eventually await ensureGatewayProfile(), but by then the session state is already cleared and the fresh chat is rendered as ready

The same bug exists in newSessionInProfile().

Fix

Make selectProfile and newSessionInProfile async. When a real profile switch is happening (switching === true), await the gateway swap before calling requestFreshSession(). For the no-switch case (re-tapping the same profile), keep the fire-and-forget as a keepalive ping.

Changes

-export function selectProfile(name: string): void {
+export async function selectProfile(name: string): Promise<void> {
   ...
   if (switching) {
+    await ensureGatewayProfile(target)
     requestFreshSession()
-  }
-  void ensureGatewayProfile(target)
+  } else {
+    void ensureGatewayProfile(target)
+  }
}

-export function newSessionInProfile(name: string): void {
+export async function newSessionInProfile(name: string): Promise<void> {
   ...
+  await ensureGatewayProfile(target)
   requestFreshSession()
-  void ensureGatewayProfile(target)
 }

Related

…resh session

The selectProfile and newSessionInProfile functions called
ensureGatewayProfile() with  (fire-and-forget), then
immediately called requestFreshSession() to navigate to a new chat.
This races with the async gateway swap — the UI shows the new
profile as active and creates a fresh chat draft, but the WebSocket
connection to the target profile's backend may not be connected yet.

When the user types a message, createBackendSessionForSend does await
the gateway swap, but by then startFreshSessionDraft() has already
cleared state and the session is created on whichever backend was
still active.

Fix: await ensureGatewayProfile(target) before calling
requestFreshSession() when a real profile switch is happening.
For the no-switch case (re-tapping the same profile), keep the
fire-and-forget as a keepalive ping.
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the targeted profile-routing investigation. This is now redundant because current main fixes the wrong-profile behavior at the session-creation chokepoint rather than by delaying fresh-draft navigation.

  • apps/desktop/src/app/session/hooks/use-session-actions/index.ts:194-195 resolves the selected profile and awaits ensureGatewayProfile() before creating a session.
  • apps/desktop/src/app/session/hooks/use-session-actions/index.ts:218-226 sends that profile explicitly in session.create, so backend ownership does not depend on the ProfileRail reset timing.
  • apps/desktop/src/app/session/hooks/use-session-actions.test.tsx:154-161 covers explicit per-profile creation routing.
  • This was implemented in 79c3ed3cc91a53e910f403e7f026a1f4ef9c1f1c (fix(desktop): new chat honours the active profile instead of rubberbanding to default, shipped in v2026.6.19).

Automated hermes-sweeper review.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state 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 labels Jul 14, 2026
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:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:implemented-on-main Sweeper: behavior already present on current main 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.

3 participants