fix(tui): track canonical model presets

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent 9d7453a53e
commit e77eed76c9
11 changed files with 339 additions and 18 deletions
+32 -3
View File
@@ -52,6 +52,7 @@ from nanobot.security.workspace_access import (
WorkspaceScopeError,
)
from nanobot.session.goal_state import goal_state_ws_blob
from nanobot.session.model_selection import model_preset_from_metadata
from nanobot.session.webui_turns import (
clear_websocket_turn_if_current,
clear_websocket_turns,
@@ -433,6 +434,19 @@ class WebSocketChannel(BaseChannel):
self._subs.setdefault(chat_id, set()).add(connection)
self._conn_chats.setdefault(connection, set()).add(chat_id)
def _attached_model_fields(self, chat_id: str) -> dict[str, str | None]:
"""Expose the session's canonical preset on the attach handshake."""
sessions = self.gateway.session_manager
if sessions is None:
return {}
snapshot = sessions.read_session_metadata(f"websocket:{chat_id}")
metadata = snapshot.get("metadata") if isinstance(snapshot, dict) else None
try:
return {"model_preset": model_preset_from_metadata(metadata)}
except ValueError:
self.logger.warning("ignoring invalid model preset metadata for chat_id={}", chat_id)
return {"model_preset": None}
def _detach(self, connection: ServerConnection, chat_id: str) -> None:
chats = self._conn_chats.get(connection)
if chats is not None:
@@ -478,7 +492,12 @@ class WebSocketChannel(BaseChannel):
"""Attach and hydrate a newly created WebUI chat fork."""
scope = self._workspaces.scope_for_session_key(fork_key)
self._attach(connection, fork_id)
await self._send_event(connection, "attached", chat_id=fork_id)
await self._send_event(
connection,
"attached",
chat_id=fork_id,
**self._attached_model_fields(fork_id),
)
await self._send_event(
connection,
"session_updated",
@@ -810,7 +829,12 @@ class WebSocketChannel(BaseChannel):
return
self._workspaces.persist_scope(new_id, scope)
self._attach(connection, new_id)
await self._send_event(connection, "attached", chat_id=new_id)
await self._send_event(
connection,
"attached",
chat_id=new_id,
**self._attached_model_fields(new_id),
)
await self._send_event(
connection,
"session_updated",
@@ -861,7 +885,12 @@ class WebSocketChannel(BaseChannel):
await self._send_event(connection, "error", detail=exc.detail, chat_id=cid)
return
self._attach(connection, cid)
await self._send_event(connection, "attached", chat_id=cid)
await self._send_event(
connection,
"attached",
chat_id=cid,
**self._attached_model_fields(cid),
)
await self._hydrate_after_subscribe(cid)
return
if t == "set_sidebar_state":
@@ -45,6 +45,7 @@ from nanobot.runtime_context import RUNTIME_CONTEXT_INPUT_META, WEBUI_QUOTE_SOUR
from nanobot.security.workspace_access import WORKSPACE_SCOPE_METADATA_KEY
from nanobot.session import webui_turns as wth
from nanobot.session.manager import SessionManager
from nanobot.session.model_selection import SESSION_MODEL_PRESET_METADATA_KEY
from nanobot.webui.gateway_services import GatewayServices, build_gateway_services
from nanobot.webui.http_utils import (
http_error as _http_error,
@@ -262,6 +263,33 @@ async def _new_temporary_chat(
return payload["chat_id"]
@pytest.mark.asyncio
async def test_attach_exposes_the_session_canonical_model_preset(bus, tmp_path) -> None:
sessions = SessionManager(tmp_path)
session = sessions.get_or_create("websocket:pinned-model")
session.metadata[SESSION_MODEL_PRESET_METADATA_KEY] = "Deep Research"
sessions.save(session)
channel = WebSocketChannel(
{"enabled": True, "allowFrom": ["*"]},
bus,
gateway=_basic_handler(bus, session_manager=sessions, workspace_path=tmp_path),
)
connection = AsyncMock()
await channel._dispatch_envelope(
connection,
"tui-client",
{"type": "attach", "chat_id": "pinned-model"},
)
payload = json.loads(connection.send.await_args_list[0].args[0])
assert payload == {
"event": "attached",
"chat_id": "pinned-model",
"model_preset": "Deep Research",
}
@pytest.mark.asyncio
async def test_temporary_chat_is_transient_and_discarded(bus, tmp_path) -> None:
sessions = SessionManager(tmp_path)