# NousResearch/hermes-agent · Desktop composer model selection reverting or not switching mid-session

## Open now

- 1 issue, 0 PRs open on GitHub.
- #65590: Desktop: can't switch model/provider mid-session (regression ~v2026.7.7)

Most members describe the Desktop app's model/provider selection not persisting or not being changeable in an existing session. Several point to session.info gateway events overwriting the composer's choice, and a couple of the items appear to be fix attempts. Reports cover different session contexts (historical, stopped, resumed Telegram), so root causes may not be identical.

## TL;DR

- 6 member(s): 1 open issue(s), 3 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #66603. Verify it addresses your case.
- **Recommended:** read PR #66603 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-66603`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.21`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #65590: Desktop: can't switch model/provider mid-session (regression ~v2026.7.7)

## Closed / duplicate issues

- #65335: Desktop: Model selector instantly reverts when resuming Telegram session from sidebar
- #65359: \[Bug\]: Desktop cannot switch models in a stopped historical session even though the composer accepts messages
- #66265: Desktop: composer model selection overridden by session.info gateway event on every poll

## Prior & competing PRs

- **#66603: fix(desktop): stop session.info from overwriting composer model**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/src/app/session/hooks/use-message-stream/composer-model-event.test.tsx`, `apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts`
  - fix hunks:
  - `apps/desktop/src/app/session/hooks/use-message-stream/composer-model-event.test.tsx`

    ```diff
    @@ -0,0 +1,99 @@
    +import { QueryClient } from '@tanstack/react-query'
    +import { act, cleanup, render, waitFor } from '@testing-library/react'
    +import { useEffect, useRef } from 'react'
    +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
    +
    +import type { ClientSessionState } from '@/app/types'
    +import { createClientSessionState } from '@/lib/chat-runtime'
    +import {
    +  $currentModel,
    +  $currentProvider,
    +  setCurrentModel,
    +  setCurrentModelSource,
    +  setCurrentProvider
    +} from '@/store/session'
    +import type { RpcEvent } from '@/types/hermes'
    +
    +import { useMessageStream } from './index'
    +
    +let handleEvent: ((event: RpcEvent) => void) | null = null
    +
    +function Harness({ activeSessionId }: { activeSessionId: string | null }) {
    +  const sessionIdRef = useRef<string | null>(activeSessionId)
    +  const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
    +  const queryClientRef = useRef(new QueryClient())
    +
    +  sessionIdRef.current = activeSessionId
    +
    +  const stream = useMessageStream({
    +    activeSessionIdRef: sessionIdRef,
    +    hydrateFromStoredSession: vi.fn(async () => undefined),
    +    queryClient: queryClientRef.current,
    +    refreshHermesConfig: vi.fn(async () => undefined),
    +    refreshSessions: vi.fn(async () => undefined),
    +    sessionStateByRuntimeIdRef,
    +    updateSessionState: (sessionId, updater) => {
    +      const current = sessionStateByRuntimeIdRef.current.get(sessionId) ?? createClientSessionState()
    +      const next = updater(current)
    +      sessionStateByRuntimeIdRef.current.set(sessionId, next)
    +
    +      return next
    +    }
    +  })
    +
    +  useEffect(() => {
    +    handleEvent = stream.handleGatewayEvent
    +  }, [stream.handleGatewayEvent])
    +
    +  return null
    +}
    +
    +async function mountStream(activeSessionId: string | null) {
    +  render(<Harness activeSessionId={activeSessionId} />)
    +  await waitFor(() => expect(handleEvent).not.toBeNull())
    +}
    +
    +describe('session.info does not clobber composer model selection', () => {
    +  beforeEach(() => {
    +    handleEvent = null
    +    setCurrentModel('deepseek-v4-flash')
    +    setCurrentProvider('deepseek')
    +    setCurrentModelSource('manual')
    +  })
    +
    +  afterEach(() => {
    +    cleanup()
    +    vi.restoreAllMocks()
    +    setCurrentModel('')
    +    setCurrentProvider('')
    +    setCurrentModelSource('')
    +  })
    +
    +  it('keeps a sticky manual pick when a global session.info carries the profile default', async () => {
    +    await mountStream(null)
    +
    +    act(() =>
    +      handleEvent!({
    +        payload: { model: 'deepseek-chat', provider: 'deepseek' },
    +        type: 'session.info'
    +      })
    +    )
    +
    +    expect($currentModel.get()).toBe('deepseek-v4-flash')
    +    expect($currentProvider.get()).toBe('deepseek')
    +  })
    +
    +  it('keeps the composer pick when an unscoped session.info arrives with no live session', async () => {
    +    await mountStream(null)
    +
    +    act(() =>
    +      handleEvent!({
    +        payload: { cwd: '/tmp/project', model: 'deepseek-chat', provider: 'deepseek' },
    +        type: 'session.info'
    +      })
    +    )
    +
    +    expect($currentModel.get()).toBe('deepseek-v4-flash')
    +    expect($currentProvider.get()).toBe('deepseek')
    +  })
    +})
    ```
  - `apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts`

    ```diff
    @@ -32,9 +32,7 @@ import {
       setCurrentBranch,
       setCurrentCwd,
       setCurrentFastMode,
    -  setCurrentModel,
       setCurrentPersonality,
    -  setCurrentProvider,
       setCurrentReasoningEffort,
       setCurrentServiceTier,
       setCurrentUsage,
    ```

    ```diff
    @@ -219,13 +217,12 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
             }
     
             if (apply) {
    -          if (modelChanged) {
    -            setCurrentModel(payload!.model || '')
    -          }
    -
    -          if (providerChanged) {
    -            setCurrentProvider(payload!.provider || '')
    -          }
    +          // Do not call setCurrentModel / setCurrentProvider here. Composer
    +          // model/provider is sticky UI state (localStorage + manual picks).
    +          // Periodic session.info heartbeats often carry the profile default
    +          // (or a stale session model) and would silently revert the dropdown.
    +          // Active-session model/provider still flows through the session state
    +          // cache via updateSessionState → syncRuntimeMetadataToView below.
     
               if (typeof payload?.cwd === 'string') {
                 // The active session's agent can relocate itself (new repo/worktree
    ```

    ```diff
    @@ -284,7 +281,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
     
             // The running→busy transition must reach EVERY session, not just the
             // active one. The `apply` gate above correctly scopes view-only side
    -        // effects (setCurrentModel, setCurrentCwd, etc.) to the focused chat,
    +        // effects (setCurrentCwd, etc.) to the focused chat,
             // but the per-session busy state is what drives the sidebar working
             // indicator — a background session's turn start/finish must update
             // its dot without the user opening it. updateSessionState only
    ```
- **#102467: fix: Desktop: can't switch model/provider mid-session (regression ~v2026.7.7) (#65590)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#102467`, `#65590`

## Fix status

- Merged to main in PR #66603 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-20`.
- Subsystem: `apps/desktop`
