Skip to content

fix(desktop): the main agent's model pick persists as the profile default - #86414

Merged
teknium1 merged 2 commits into
mainfrom
fix/primary-model-picker-persists
Aug 14, 2026
Merged

teknium1 merged 2 commits into
mainfrom
fix/primary-model-picker-persists

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Report (@rashidkhan): the default Hermes bot switches to the OpenAI API account rather than the subscription, and doesn't retain the previous selection.

Why — the actual bug. The composer model picker always sent the switch as --session scope, even for the primary profile's main agent. So the selection never wrote config.yaml model.provider. With model.provider unset, resolve_provider('auto') falls through (tier 3) to a leftover OPENAI_API_KEY in ~/.hermes/.env and picks OpenAI/OpenRouter. The subscription the user picked was only a per-session override that evaporated next session — hence "doesn't retain the selection" and "switches to OpenAI." One root cause, both symptoms.

Fix. When the pick targets the primary profile's main agent (touchesPrimary), send --global instead of --session, so it persists to config.yaml (model.default + model.provider) through the existing model-switch persist path. A set model.provider already outranks the env key in resolve_provider (tier 2 > tier 3, verified), so the main agent keeps the chosen provider across restarts.

Deliberately narrow:

  • Secondary chat tiles stay --session — picking a model in one chat must not rewrite the profile default (the cross-session-contamination guard the old comment protected). Only the primary main-agent pick persists.
  • No change to resolve_provider's priority chain — so Bug: auth.json active_provider silently overrides model.provider in config.yaml at runtime #29285 (an explicit env key beating a stale OAuth login) is untouched. We just make the user's explicit main-agent selection the config default it always should have been, rather than fighting the priority chain.

This supersedes #86386 (which tried to reorder the priority chain and collided with #29285's tested contract). Renderer change — ships in the next desktop build.

…ault

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so #29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.
@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on ec2ebb3 — MoA presets stay session-scoped; update tests for primary-pe

⚠️ Warnings

OSV vulnerability scan · View job

5 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 3m11s vs 6m6s (-47.8%). 14 job(s) slower, 3 faster, 1 unchanged.

  • JS & TS checks / apps/desktop / check:test:ui:shard-3of3: +16.0s
  • JS & TS checks / apps/desktop / check:test:ui:shard-2of3: -13.0s
  • JS & TS checks / apps/desktop / check:test:ui:shard-1of3: +11.0s
  • JS & TS checks / apps/desktop / check:test:desktop:all: +11.0s
  • OSV scan / Scan lockfiles / osv-scan: +10.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) area/config Config system, migrations, profiles P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 14, 2026
Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
@teknium1
teknium1 merged commit 7d96537 into main Aug 14, 2026
38 checks passed
@teknium1
teknium1 deleted the fix/primary-model-picker-persists branch August 14, 2026 22:40
skappafrost pushed a commit to skappafrost/hermes-agent that referenced this pull request Aug 15, 2026
…ault (NousResearch#86414)

* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so NousResearch#29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
bobaba76 pushed a commit to bobaba76/hermes-agent that referenced this pull request Aug 27, 2026
…ault (NousResearch#86414)

* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so NousResearch#29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
Codename-11 pushed a commit to Codename-11/hermes-agent that referenced this pull request Sep 2, 2026
…policy for every surface (NousResearch#90235)

Symptom: picking a model in the Desktop composer for the primary chat
silently rewrote config.yaml (model.default + model.provider) as the
profile default, ignoring model.persist_switch_by_default. A throwaway
pick that resolved to e.g. openai-api (no key) left the profile with an
unusable default on the next launch (NousResearch#90235).

Root cause: 7d96537 (NousResearch#86414) made use-model-controls.ts send --global
for every primary-tile pick so a fresh profile would get a persisted
provider instead of falling through to a leftover OPENAI_API_KEY env var.
That put a persistence policy in the client, contradicting the
server-side rule /model uses (resolve_persist_behavior).

Fix:
- resolve_persist_behavior gains one rule, ahead of the --provider
  session-only rule: when neither model.default nor model.provider is
  configured yet, persist. This preserves NousResearch#86414's first-pick motivation
  for CLI, gateway and Desktop alike. With a default configured, a plain
  pick is session-only unless --global / persist_switch_by_default.
- Desktop primary-tile picks send no scope flag and let the gateway decide.
  Secondary tiles and MoA presets still send --session.
- /model help text in cli.py said "(persists)"; it now matches reality and
  lists --global.
- Docs: desktop.md picker note + slash-commands /model row.

Tests: test_first_pick_persists_then_session_only (fails on main), and the
existing use-model-controls vitest updated to assert the flag-less request.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…ault (NousResearch#86414)

* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so NousResearch#29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…policy for every surface (NousResearch#90235)

Symptom: picking a model in the Desktop composer for the primary chat
silently rewrote config.yaml (model.default + model.provider) as the
profile default, ignoring model.persist_switch_by_default. A throwaway
pick that resolved to e.g. openai-api (no key) left the profile with an
unusable default on the next launch (NousResearch#90235).

Root cause: 7d96537 (NousResearch#86414) made use-model-controls.ts send --global
for every primary-tile pick so a fresh profile would get a persisted
provider instead of falling through to a leftover OPENAI_API_KEY env var.
That put a persistence policy in the client, contradicting the
server-side rule /model uses (resolve_persist_behavior).

Fix:
- resolve_persist_behavior gains one rule, ahead of the --provider
  session-only rule: when neither model.default nor model.provider is
  configured yet, persist. This preserves NousResearch#86414's first-pick motivation
  for CLI, gateway and Desktop alike. With a default configured, a plain
  pick is session-only unless --global / persist_switch_by_default.
- Desktop primary-tile picks send no scope flag and let the gateway decide.
  Secondary tiles and MoA presets still send --session.
- /model help text in cli.py said "(persists)"; it now matches reality and
  lists --global.
- Docs: desktop.md picker note + slash-commands /model row.

Tests: test_first_pick_persists_then_session_only (fails on main), and the
existing use-model-controls vitest updated to assert the flag-less request.
adurham added a commit to adurham/hermes-agent that referenced this pull request Sep 26, 2026
…wiring

Bare `/reasoning` (and the `/effort` alias added for Claude Code parity) set
`_reasoning_picker_state` and then painted nothing, while the app's input filter
still excluded that state — a locked composer behind an invisible modal. The
picker's state machine, render helper and Enter handler survived the upstream
merges; the four pieces that connect it to the app did not.

Lost at the v2026.7.20 sync and dropped from every sync since:
`git show <tag>:cli.py | grep -c _get_reasoning_picker_display` is 2 at
ff97264/a4c788a9a9, 0 from v2026.7.20 onward. Restored on the mixin that owns
the layout now (hermes_cli/cli_tui_mixin.py):

* `_get_reasoning_picker_display_fragments` renders through the same
  `_render_scroll_list_panel` as the model picker and command palette, so the
  picker inherits the shared viewport/wrap/chrome behaviour instead of the
  original commit's hand-rolled panel (whose helpers no longer exist).
* `_tui_reasoning_picker_{up,down,escape}` + their `kb.add(...)` bindings. Enter
  is deliberately NOT bound: submission goes through `_tui_enter_overlay`, exactly
  like the model picker.
* `reasoning_picker_widget` built in `_tui_build_layout` and passed into
  `_build_tui_layout_children` — a parameter that had survived the whole time with
  nothing to fill it.
* `_reasoning_picker_state` added to both `_tui_cancel_foreground_ui` closer lists
  (Ctrl+C / Ctrl+Q) so the picker always has an escape hatch.

Apply semantics: `_apply_reasoning_arg` gains `persist_global: bool = False`. The
original helper wrote the GLOBAL `agent.reasoning_effort` unconditionally on every
pick; upstream has since made the same command's typed form session-scoped with
`--global` (NousResearch#86414) and the picker has no scope affordance, so restoring that write
verbatim would ship one command with two persistence policies. The per-model map
write (`agent.reasoning_effort_by_model`) is deliberately NOT gated — it is this
fork's isolation feature, this helper is its only writer, and its reader
(`_apply_reasoning_for_new_model`) runs on every model switch, so gating it would
leave the documented feature write-dead. The confirmation line now names the scope
it actually used (`session; default for <model>`).

Verified: 24 new tests in tests/hermes_cli/test_reasoning_picker.py, fail-first
proven against a pristine `git archive HEAD` export of the pre-fix tree (11 failed
there, 24/24 pass now). Two standalone probes drove the real layout assembly and a
real prompt_toolkit KeyBindings registry: the widget lands immediately after the
model picker with its filter tracking the state, and the registry resolves
Up/Down/Escape to the picker handlers while open (none while closed; Escape eager).
Existing reasoning/extension-hook suites green (44 passed across 4 files). FORK.md
entry added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants