Skip to content

fix(gateway): thread session overrides through Bot Chat capability rebuild - #88826

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/bot-chat-capability-sync-model-override
Open

fix(gateway): thread session overrides through Bot Chat capability rebuild#88826
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/bot-chat-capability-sync-model-override

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

What

tui_gateway/server.py::_sync_bot_capabilities() rebuilds a Bot Chat session's agent in place whenever its capability surface changes (skill install, MCP toggle, SOUL edit). It does so via a bare call:

new_agent = _make_agent(
    sid,
    session["session_key"],
    session_id=session["session_key"],
    platform_override=_session_source(session),
)

Unlike _make_agent's other caller (the initial session-build path, which threads current.get("model_override") / create_reasoning_override / create_service_tier_override into the kwargs), this rebuild never passes them — so _make_agent falls back to the global config default for the model, reasoning effort, and service tier.

Why it matters

Bot Mode's own premise is that each bot keeps its own model. Concretely: pin a bot to a specific model via /model X (persisted as session["model_override"]), then install a skill or toggle an MCP server for that bot later — the very next turn silently reverts it to the config-default model. No error, just a generic "Capabilities updated — this bot's tools and prompt were refreshed." notice.

Second, related gap: the /model --once race

Two lines above the _sync_bot_capabilities call, _apply_pending_model_switch/_sync_agent_model_with_config are already wrapped in if not one_turn_restore:, with a comment citing #29923: a /model --once turn mutates the live agent object in place and schedules a post-turn restore, so a same-turn rebuild must not run or it clobbers the in-place switch. _sync_bot_capabilities sat outside that guard, so a capability-triggered rebuild landing during a pending once-restore replaces the agent object outright — discarding the once-switch, and (per the mutation-verify run below) handing the turn to a brand-new agent object.

Fix

  • Thread model_override / reasoning_config_override / service_tier_override from the session dict into _sync_bot_capabilities's _make_agent call, mirroring the initial-build caller.
  • Move the _sync_bot_capabilities(sid, session) call inside the existing if not one_turn_restore: block. The capability edit is still adopted on the very next non-once turn — bot_caps_seen is only advanced once the guarded branch actually runs.

Testing

Added 4 tests to tests/test_tui_gateway_server.py:

  • test_bot_capability_sync_threads_session_overrides_into_rebuild — a pinned model/reasoning/tier survive a capability-triggered rebuild.
  • test_bot_capability_sync_rebuild_without_pin_omits_override_kwargs — an unpinned bot doesn't get override kwargs manufactured out of nothing.
  • test_bot_capability_sync_noop_when_fingerprint_unchanged — no rebuild when the capability fingerprint hasn't moved.
  • test_prompt_submit_skips_bot_capability_rebuild_during_pending_once_restore — drives the real prompt.submit dispatch with a pending one_turn_model_restore; _make_agent must not be called during that turn.

Mutation-verified (fix reverted via git stash):

  • The threading test fails — _make_agent gets called without the override kwargs.
  • The once-restore test fails — _make_agent gets called at all inside the guarded window (and, revealingly, handing the turn to the freshly rebuilt agent crashes the mocked turn with AttributeError: ... no attribute 'run_conversation', since the fake rebuild returns a bare object — a concrete illustration of what an in-flight rebuild does to a running turn).

Full tests/test_tui_gateway_server.py (589 tests) and tests/tui_gateway/ + tests/tools/test_bot_mode_probe.py (499 tests) pass. ruff check clean on both changed files.

Checklist

  • Tests added/updated
  • Mutation-verified (fix reverted → both target tests fail)
  • No behavior change for non-Bot-Chat sessions (function returns before reaching the changed code) or for a Bot Chat with no pin (override kwargs stay empty, matching current behavior)

…build

_sync_bot_capabilities() rebuilds a Bot Chat session's agent in place
when its capability surface changes (skill install, MCP toggle, SOUL
edit), via a bare _make_agent(sid, key, session_id=..., platform_
override=...) call. Unlike _make_agent's other caller (the initial
session-build path), it never threads model_override / reasoning_config_
override / service_tier_override from the session dict, so _make_agent
falls back to the global config default for each of them.

Concretely: a bot pinned to a specific model via `/model X` (persisted
as session["model_override"]) silently reverted to the config-default
model the next time a capability edit landed for that bot -- directly
undermining Bot Mode's own premise that each bot keeps its own model.
No error, no warning beyond a generic "Capabilities updated" notice.

Also guard the call site with the same `if not one_turn_restore:` check
that already wraps _apply_pending_model_switch/_sync_agent_model_with_
config two lines above, for the same reason cited there (NousResearch#29923): a
/model --once turn mutates the live agent in place and schedules a
post-turn restore. A capability-triggered rebuild landing inside that
window replaces the agent object outright, discarding the in-place
once-switch (and, as the mutation-verify run showed, handing the turn
to a freshly built agent that had no run_conversation wired for the
mocked test double -- a real capability-sync-during-once-turn race
would hand it a normal agent, but still one that silently dropped the
once-model). The capability edit is still adopted on the next non-once
turn; bot_caps_seen is only advanced once the guarded branch runs.

Added 4 regression tests: two exercise _sync_bot_capabilities directly
(override threading present/absent), one confirms the no-op path when
the capability fingerprint hasn't moved, one drives the full prompt.
submit dispatch to prove the rebuild is skipped while a once-restore is
pending. Mutation-verified: reverting the fix makes the threading test
fail (_make_agent called without the override kwargs) and the once-
restore test fail (_make_agent gets called at all during the guarded
window).
@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) 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.

2 participants