Repository navigation
Description
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):
- Configure a default model whose credential is invalid, or that belongs to a different provider than the one you switch to.
- In a TUI or web session, switch the conversation's model to a working provider (a per-conversation switch, not
--default). - Ask the main agent to spawn a built-in sub-agent instance. It runs on the switched model.
- 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:837takesreq.conversationas the session key. For a direct chat that value is the lanesession#agent/handle(direct_lane,raven/spine/turn.py:110).main.py:857passes it tobinding_for_session(raven/agent/loop/wiring.py:554), which looks up_session_bindingsby 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
akushonkamen commentedon Oct 10, 2026
Confirmed the root cause: a direct chat's
req.conversationis the lane{session}#{agent}/{handle}(direct_lane,raven/spine/turn.py:110), butraven/agent/loop/main.pyfeeds it straight intobinding_for_session(raven/agent/loop/wiring.py:554), which keys_session_bindingsby the bare session key thatconfig.set key=modelstores 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()(asturn_path.py:3065andrpc/spine.py:360already do), plus a test asserting a direct-lane turn picks up the session's switched binding.(Submitted with AI assistance, disclosed per AGENTS.md.)
akushonkamen commentedon Oct 10, 2026
The fix is up: #902 --
binding_for_sessioninraven/agent/loop/main.pynow resolves the lane back to its session viasession_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.fix(agent): resolve a direct chat's model binding through its session (…
fix(agent): resolve a direct chat's model binding through its session (…