diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 4a9947e4a..e137a61c0 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -528,6 +528,12 @@ class AgentLoop: effective_key = UNIFIED_SESSION_KEY else: effective_key = f"{channel}:{chat_id}" + effective_key = self._tool_context_session_key( + channel=channel, + chat_id=chat_id, + metadata=metadata, + session_key=effective_key, + ) request_ctx = RequestContext( channel=channel, @@ -542,6 +548,24 @@ class AgentLoop: if tool and isinstance(tool, ContextAware): tool.set_context(request_ctx) + def _tool_context_session_key( + self, + *, + channel: str, + chat_id: str, + metadata: dict | None, + session_key: str, + ) -> str: + """Return the session key tools should use for ownership-scoped resources.""" + if ( + self._unified_session + and channel == "websocket" + and (metadata or {}).get("webui") is True + and chat_id + ): + return f"websocket:{chat_id}" + return session_key + @staticmethod def _runtime_chat_id(msg: InboundMessage) -> str: """Return the chat id shown in runtime metadata for the model.""" diff --git a/tests/test_tool_contextvars.py b/tests/test_tool_contextvars.py index a72296d67..3826ba37f 100644 --- a/tests/test_tool_contextvars.py +++ b/tests/test_tool_contextvars.py @@ -4,6 +4,7 @@ import asyncio import pytest +from nanobot.agent.loop import AgentLoop from nanobot.agent.tools.context import RequestContext from nanobot.agent.tools.cron import CronTool from nanobot.agent.tools.message import MessageTool @@ -243,6 +244,35 @@ async def test_cron_tool_basic_set_context_and_execute(tmp_path) -> None: assert jobs[0].payload.session_key == "wechat:user-789" +@pytest.mark.asyncio +async def test_webui_cron_tool_uses_visible_session_under_unified_session(tmp_path) -> None: + """WebUI-created automations should attach to the visible thread, not unified memory.""" + tool = CronTool(CronService(tmp_path / "jobs.json")) + + class _Tools: + tool_names = ["cron"] + + def get(self, name: str): + return tool if name == "cron" else None + + loop = object.__new__(AgentLoop) + loop._unified_session = True + loop.tools = _Tools() + loop._set_tool_context( + "websocket", + "chat-123", + metadata={"webui": True}, + session_key="unified:default", + ) + + result = await tool.execute(action="add", message="standup", every_seconds=300) + assert result.startswith("Created job") + + jobs = tool._cron.list_jobs() + assert len(jobs) == 1 + assert jobs[0].payload.session_key == "websocket:chat-123" + + @pytest.mark.asyncio async def test_cron_tool_no_context_returns_error(tmp_path) -> None: """Without set_context, add should fail with a clear error."""