Skip to content

fix(agent): a direct chat with a built-in sub-agent instance ignores the session's model switch #888

Closed

Description

@0xKT

Summary

After a conversation switches its model (the per-conversation /model or the page's model picker), a direct chat with a built-in sub-agent instance in that conversation still runs on the configured default model binding. The main agent and the sub-agents it spawns use the session's choice; the direct chat does not. When the default credential is invalid, the direct chat fails with the provider's 401 while the rest of the session works. This is the symptom #281 reported for spawn (fixed by #282 and #284), reached through a different entry point.

Steps to reproduce

User-level scenario, derived from the code path below (verified with the probe in the logs section, not end to end in the UI):

  1. Configure a default model whose credential is invalid, or that belongs to a different provider than the one you switch to.
  2. In a TUI or web session, switch the conversation's model to a working provider (a per-conversation switch, not --default).
  3. Ask the main agent to spawn a built-in sub-agent instance. It runs on the switched model.
  4. Open a direct chat with that instance and send a message. It runs on the default model.

Expected behavior

A direct chat turn runs on its session's binding, the same pair the main agent and spawned sub-agents use. CONTEXT.md defines a Direct Chat as a sub-conversation of its session (see Lane), and the Subagent working directory entry already resolves the session's workdir for a Direct Chat.

Actual behavior

The direct chat runs on the default binding (agents.defaults) and ignores the session's switch. A model switch (config.set with key: model) stores the binding under the session_id the client sends (raven/rpc/methods/config.py:630), and both the TUI's /model (ui-tui/src/app/slash/commands/session.ts:127) and the page's picker (ui-web/src/features/model/source.ts:222) send the session id, so a lane never gets an entry of its own and always resolves to the default.

Environment

Code path, not platform specific. Reproduced on main be7537c (2026-10-09). Affects built-in (RavenLoop) instances only; third-party ACP and CLI agents run their own model.

Logs or screenshots

Probe on main be7537c with stub providers and the network blocked: the default is bound to provider DEFAULT_DEAD and session tui:judge is switched to SESSION_GOOD. An in-turn spawn runs on SESSION_GOOD; a direct chat on the instance's lane does not.

A  DEFAULT_DEAD calls: [] | SESSION_GOOD calls: ['main', 'main', 'subagent']
C lane: tui:judge#Raven/h1 | binding(session) = SESSION_GOOD | binding(lane) = DEFAULT_DEAD
C direct reply: ['DEFAULT_DEAD:direct:ok']
C  DEFAULT_DEAD calls: ['direct'] | SESSION_GOOD calls: []

Root cause

  • raven/agent/loop/main.py:837 takes req.conversation as the session key. For a direct chat that value is the lane session#agent/handle (direct_lane, raven/spine/turn.py:110).
  • main.py:857 passes it to binding_for_session (raven/agent/loop/wiring.py:554), which looks up _session_bindings by that exact key. The session's switch is stored under the bare session key, so the lookup misses and falls back to the default binding.
  • Direct chat lanes landed on 2026-08-17 (32f8044); per-conversation binding landed on 2026-08-19 (feat(agent): bind the model to the conversation, not the process #284) and did not cover lanes.

Suggested fix

Resolve the binding through session_of() (raven/spine/turn.py:121), as turn_path.py:3065 and rpc/spine.py:360 already do for lanes, and add a test that switches a session's binding and asserts that a direct-lane turn uses it. No test covers the binding of a direct lane today.

Open question for maintainers: should a direct chat follow the session's current binding (suggested here) or the instance's spawn-time binding? The two differ only when the session switches again after the spawn; neither is the default.

Related: #281

Activity

  1. added
    bugSomething isn't working
    on Oct 9, 2026
  2. akushonkamen commented on Oct 10, 2026

    @akushonkamen
    Contributor

    Confirmed the root cause: a direct chat's req.conversation is the lane {session}#{agent}/{handle} (direct_lane, raven/spine/turn.py:110), but raven/agent/loop/main.py feeds it straight into binding_for_session (raven/agent/loop/wiring.py:554), which keys _session_bindings by the bare session key that config.set key=model stores under. The lookup misses and falls back to the default binding. Same disease as #281, entering via the direct-chat turn path this time.

    Working on a fix: normalize the binding lookup through session_of() (as turn_path.py:3065 and rpc/spine.py:360 already do), plus a test asserting a direct-lane turn picks up the session's switched binding.

    (Submitted with AI assistance, disclosed per AGENTS.md.)

  3. akushonkamen commented on Oct 10, 2026

    @akushonkamen
    Contributor

    The fix is up: #902 -- binding_for_session in raven/agent/loop/main.py now resolves the lane back to its session via session_of (same as the record path and the rpc spine), with a red-to-green test asserting a direct-lane turn runs on the session's switched binding. Exact commands and results are in the PR description.

  4. added 2 commits that reference this issue on Oct 10, 2026
    6126965
    4a4b5ff
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions