refactor: centralize cron session metadata keys

This commit is contained in:
chengyongru 2026-06-12 11:43:23 +08:00
parent 0e3a57b371
commit d9d481bc15
18 changed files with 116 additions and 62 deletions

View File

@ -130,7 +130,7 @@ Instead, persist a readable automation trigger event, for example:
{ {
"role": "user", "role": "user",
"content": "Scheduled automation triggered: daily monitor\n\nCheck ...", "content": "Scheduled automation triggered: daily monitor\n\nCheck ...",
"_automation_trigger": true, "_automation_turn": true,
"automation_id": "abc123", "automation_id": "abc123",
"automation_name": "daily monitor", "automation_name": "daily monitor",
"automation_run_id": "abc123:1770000000000", "automation_run_id": "abc123:1770000000000",

View File

@ -40,6 +40,7 @@ from nanobot.bus.runtime_events import (
from nanobot.command import CommandContext, CommandRouter, register_builtin_commands from nanobot.command import CommandContext, CommandRouter, register_builtin_commands
from nanobot.config.schema import AgentDefaults, ModelPresetConfig from nanobot.config.schema import AgentDefaults, ModelPresetConfig
from nanobot.cron.automation import ( from nanobot.cron.automation import (
AUTOMATION_HISTORY_META,
automation_run_id, automation_run_id,
automation_trigger, automation_trigger,
defer_until_session_idle, defer_until_session_idle,
@ -57,6 +58,7 @@ from nanobot.session.goal_state import (
runner_wall_llm_timeout_s, runner_wall_llm_timeout_s,
sustained_goal_active, sustained_goal_active,
) )
from nanobot.session.keys import UNIFIED_SESSION_KEY, session_key_for_channel
from nanobot.session.manager import Session, SessionManager from nanobot.session.manager import Session, SessionManager
from nanobot.session.routing import persist_routing_context from nanobot.session.routing import persist_routing_context
from nanobot.utils.document import extract_documents, reference_non_image_attachments from nanobot.utils.document import extract_documents, reference_non_image_attachments
@ -78,8 +80,6 @@ if TYPE_CHECKING:
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
UNIFIED_SESSION_KEY = "unified:default"
class TurnState(Enum): class TurnState(Enum):
RESTORE = auto() RESTORE = auto()
COMPACT = auto() COMPACT = auto()
@ -522,12 +522,11 @@ class AgentLoop:
"""Update context for all tools that need routing info.""" """Update context for all tools that need routing info."""
from nanobot.agent.tools.context import ContextAware from nanobot.agent.tools.context import ContextAware
if session_key is not None: effective_key = session_key or session_key_for_channel(
effective_key = session_key channel,
elif self._unified_session: chat_id,
effective_key = UNIFIED_SESSION_KEY unified_session=self._unified_session,
else: )
effective_key = f"{channel}:{chat_id}"
request_ctx = RequestContext( request_ctx = RequestContext(
channel=channel, channel=channel,
chat_id=chat_id, chat_id=chat_id,
@ -646,7 +645,7 @@ class AgentLoop:
if isinstance(persist_content, str) and persist_content.strip(): if isinstance(persist_content, str) and persist_content.strip():
text = persist_content text = persist_content
extra.update({ extra.update({
"_automation_trigger": True, AUTOMATION_HISTORY_META: True,
"automation_id": trigger.get("job_id"), "automation_id": trigger.get("job_id"),
"automation_name": trigger.get("job_name"), "automation_name": trigger.get("job_name"),
"automation_run_id": trigger.get("run_id"), "automation_run_id": trigger.get("run_id"),

View File

@ -142,7 +142,7 @@ class CronTool(Tool, ContextAware):
if action == "add": if action == "add":
if self._in_cron_context.get(): if self._in_cron_context.get():
return "Error: cannot schedule new jobs from within a cron job execution" return "Error: cannot schedule new jobs from within a cron job execution"
return self._add_job(name, message, every_seconds, cron_expr, tz, at, deliver) return self._add_job(name, message, every_seconds, cron_expr, tz, at)
elif action == "list": elif action == "list":
return self._list_jobs() return self._list_jobs()
elif action == "remove": elif action == "remove":
@ -157,7 +157,6 @@ class CronTool(Tool, ContextAware):
cron_expr: str | None, cron_expr: str | None,
tz: str | None, tz: str | None,
at: str | None, at: str | None,
deliver: bool = True,
) -> str: ) -> str:
if not message: if not message:
return ( return (

View File

@ -64,6 +64,10 @@ from nanobot.utils.restart import ( # noqa: E402
format_restart_completed_message, format_restart_completed_message,
should_show_cli_restart_notice, should_show_cli_restart_notice,
) )
from nanobot.webui.metadata import ( # noqa: E402
WEBUI_MESSAGE_SOURCE_METADATA_KEY,
WEBUI_TURN_METADATA_KEY,
)
def _sanitize_surrogates(text: str) -> str: def _sanitize_surrogates(text: str) -> str:
@ -89,8 +93,6 @@ class SafeFileHistory(FileHistory):
super().store_string(_sanitize_surrogates(string)) super().store_string(_sanitize_surrogates(string))
_WEBUI_TURN_META_KEY = "webui_turn_id"
_WEBUI_MESSAGE_SOURCE_META_KEY = "_webui_message_source"
_PROACTIVE_WEBUI_METADATA: ContextVar[dict[str, Any] | None] = ContextVar( _PROACTIVE_WEBUI_METADATA: ContextVar[dict[str, Any] | None] = ContextVar(
"proactive_webui_metadata", "proactive_webui_metadata",
default=None, default=None,
@ -106,13 +108,13 @@ def _proactive_delivery_metadata(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return channel metadata for a fresh proactive delivery turn.""" """Return channel metadata for a fresh proactive delivery turn."""
out = dict(metadata or {}) out = dict(metadata or {})
out.pop(_WEBUI_TURN_META_KEY, None) out.pop(WEBUI_TURN_METADATA_KEY, None)
if channel == "websocket": if channel == "websocket":
out[_WEBUI_TURN_META_KEY] = f"{turn_seed}:{uuid.uuid4().hex}" out[WEBUI_TURN_METADATA_KEY] = f"{turn_seed}:{uuid.uuid4().hex}"
source: dict[str, str] = {"kind": "cron"} source: dict[str, str] = {"kind": "cron"}
if source_label: if source_label:
source["label"] = source_label source["label"] = source_label
out[_WEBUI_MESSAGE_SOURCE_META_KEY] = source out[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source
return out return out
app = typer.Typer( app = typer.Typer(
@ -1034,14 +1036,14 @@ def _run_gateway(
schedule_background=lambda coro: agent._schedule_background(coro), schedule_background=lambda coro: agent._schedule_background(coro),
).subscribe(runtime_events) ).subscribe(runtime_events)
from nanobot.agent.loop import UNIFIED_SESSION_KEY
from nanobot.bus.events import InboundMessage, OutboundMessage from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.session.keys import session_key_for_channel
def _channel_session_key(channel: str, chat_id: str) -> str: def _channel_session_key(channel: str, chat_id: str) -> str:
return ( return session_key_for_channel(
UNIFIED_SESSION_KEY channel,
if config.agents.defaults.unified_session chat_id,
else f"{channel}:{chat_id}" unified_session=config.agents.defaults.unified_session,
) )
def _session_metadata(session_key: str) -> dict[str, Any]: def _session_metadata(session_key: str) -> dict[str, Any]:
@ -1125,17 +1127,20 @@ def _run_gateway(
), ),
} }
metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] = True metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] = True
run_record_base: dict[str, Any] = {
"job_id": job.id,
"job_name": job.name,
"session_key": session_key,
"prompt_ref": prompt_ref,
"prompt_vars": {"message": job.payload.message},
"rendered_prompt": prompt,
}
cron.write_run_record( cron.write_run_record(
run_id, run_id,
{ {
"job_id": job.id, **run_record_base,
"job_name": job.name,
"session_key": session_key,
"status": "queued", "status": "queued",
"prompt_ref": prompt_ref,
"prompt_vars": {"message": job.payload.message},
"rendered_prompt": prompt,
}, },
) )
@ -1159,14 +1164,9 @@ def _run_gateway(
cron.write_run_record( cron.write_run_record(
run_id, run_id,
{ {
"job_id": job.id, **run_record_base,
"job_name": job.name,
"session_key": session_key,
"status": "error", "status": "error",
"error": error_text, "error": error_text,
"prompt_ref": prompt_ref,
"prompt_vars": {"message": job.payload.message},
"rendered_prompt": prompt,
}, },
) )
raise raise
@ -1178,13 +1178,8 @@ def _run_gateway(
cron.write_run_record( cron.write_run_record(
run_id, run_id,
{ {
"job_id": job.id, **run_record_base,
"job_name": job.name,
"session_key": session_key,
"status": "ok", "status": "ok",
"prompt_ref": prompt_ref,
"prompt_vars": {"message": job.payload.message},
"rendered_prompt": prompt,
"response": response, "response": response,
}, },
) )

View File

@ -8,6 +8,7 @@ from nanobot.cron.types import CronJob
AUTOMATION_TRIGGER_META = "_automation_trigger" AUTOMATION_TRIGGER_META = "_automation_trigger"
AUTOMATION_DEFER_UNTIL_IDLE_META = "_defer_until_session_idle" AUTOMATION_DEFER_UNTIL_IDLE_META = "_defer_until_session_idle"
AUTOMATION_HISTORY_META = "_automation_turn"
def automation_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None: def automation_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:

12
nanobot/session/keys.py Normal file
View File

@ -0,0 +1,12 @@
"""Shared session key constants and helpers."""
from __future__ import annotations
UNIFIED_SESSION_KEY = "unified:default"
def session_key_for_channel(channel: str, chat_id: str, *, unified_session: bool = False) -> str:
"""Return the session key for a channel/chat pair."""
if unified_session:
return UNIFIED_SESSION_KEY
return f"{channel}:{chat_id}"

View File

@ -14,6 +14,7 @@ from typing import Any
from loguru import logger from loguru import logger
from nanobot.config.paths import get_legacy_sessions_dir from nanobot.config.paths import get_legacy_sessions_dir
from nanobot.session.metadata import SESSION_ROUTING_METADATA_KEY
from nanobot.utils.helpers import ( from nanobot.utils.helpers import (
ensure_dir, ensure_dir,
estimate_message_tokens, estimate_message_tokens,
@ -36,7 +37,7 @@ _FORK_VOLATILE_METADATA_KEYS = {
"pending_user_turn", "pending_user_turn",
"runtime_checkpoint", "runtime_checkpoint",
"thread_goal", "thread_goal",
"_routing_context", SESSION_ROUTING_METADATA_KEY,
"title", "title",
"title_user_edited", "title_user_edited",
} }

View File

@ -0,0 +1,3 @@
"""Shared session metadata keys."""
SESSION_ROUTING_METADATA_KEY = "_routing_context"

View File

@ -7,8 +7,7 @@ from typing import Any, Mapping
from nanobot.bus.events import InboundMessage from nanobot.bus.events import InboundMessage
from nanobot.cron.automation import is_automation_turn from nanobot.cron.automation import is_automation_turn
from nanobot.session.manager import Session from nanobot.session.manager import Session
from nanobot.session.metadata import SESSION_ROUTING_METADATA_KEY
SESSION_ROUTING_METADATA_KEY = "_routing_context"
_ROUTING_METADATA_KEYS = { _ROUTING_METADATA_KEYS = {
"chat_type", "chat_type",

View File

@ -0,0 +1,4 @@
"""Shared WebUI metadata keys."""
WEBUI_TURN_METADATA_KEY = "webui_turn_id"
WEBUI_MESSAGE_SOURCE_METADATA_KEY = "_webui_message_source"

View File

@ -18,6 +18,7 @@ from loguru import logger
from nanobot.config.paths import get_webui_dir from nanobot.config.paths import get_webui_dir
from nanobot.session.manager import SessionManager from nanobot.session.manager import SessionManager
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
WEBUI_TRANSCRIPT_SCHEMA_VERSION = 3 WEBUI_TRANSCRIPT_SCHEMA_VERSION = 3
WEBUI_FORK_MARKER_EVENT = "fork_marker" WEBUI_FORK_MARKER_EVENT = "fork_marker"
@ -29,8 +30,6 @@ _TRANSCRIPT_SEGMENT_RE = re.compile(r"^\d{6}\.jsonl$")
_DEFAULT_TRANSCRIPT_PAGE_LIMIT = 160 _DEFAULT_TRANSCRIPT_PAGE_LIMIT = 160
_MAX_TRANSCRIPT_PAGE_LIMIT = 1000 _MAX_TRANSCRIPT_PAGE_LIMIT = 1000
_WEBUI_TURN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$") _WEBUI_TURN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
WEBUI_TURN_METADATA_KEY = "webui_turn_id"
WEBUI_MESSAGE_SOURCE_METADATA_KEY = "_webui_message_source"
_MARKDOWN_LOCAL_IMAGE_RE = re.compile( _MARKDOWN_LOCAL_IMAGE_RE = re.compile(
r"!\[([^\]]*)\]\((<[^>]+>|[^)\s]+)(\s+(?:\"[^\"]*\"|'[^']*'))?\)" r"!\[([^\]]*)\]\((<[^>]+>|[^)\s]+)(\s+(?:\"[^\"]*\"|'[^']*'))?\)"
) )

View File

@ -20,8 +20,8 @@ from loguru import logger
from websockets.http11 import Request as WsRequest from websockets.http11 import Request as WsRequest
from websockets.http11 import Response from websockets.http11 import Response
from nanobot.agent.loop import UNIFIED_SESSION_KEY
from nanobot.command.builtin import builtin_command_palette from nanobot.command.builtin import builtin_command_palette
from nanobot.session.keys import UNIFIED_SESSION_KEY
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload

View File

@ -8,6 +8,7 @@ from nanobot.agent.context import ContextBuilder
from nanobot.agent.loop import AgentLoop from nanobot.agent.loop import AgentLoop
from nanobot.bus.events import InboundMessage from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.cron.automation import AUTOMATION_HISTORY_META, AUTOMATION_TRIGGER_META
from nanobot.providers.base import LLMResponse from nanobot.providers.base import LLMResponse
from nanobot.session.goal_state import GOAL_STATE_KEY from nanobot.session.goal_state import GOAL_STATE_KEY
from nanobot.session.manager import Session, SessionManager from nanobot.session.manager import Session, SessionManager
@ -65,6 +66,41 @@ def test_agent_loop_llm_runtime_reflects_current_provider_and_model(tmp_path: Pa
assert runtime.model == "next-model" assert runtime.model == "next-model"
def test_persist_automation_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
loop = _make_full_loop(tmp_path)
session = loop.sessions.get_or_create("websocket:auto")
prompt_ref = {"id": "cron.agent_turn.reminder", "version": 1, "sha256": "abc"}
persisted = loop._persist_user_message_early(
InboundMessage(
channel="websocket",
sender_id="cron",
chat_id="auto",
content="Automation: internal prompt",
metadata={
AUTOMATION_TRIGGER_META: {
"job_id": "job-1",
"job_name": "Daily check",
"run_id": "job-1:1",
"prompt_ref": prompt_ref,
"persist_content": "Scheduled automation triggered: Daily check",
}
},
),
session,
)
assert persisted is True
message = session.messages[-1]
assert message["content"] == "Scheduled automation triggered: Daily check"
assert message[AUTOMATION_HISTORY_META] is True
assert AUTOMATION_TRIGGER_META not in message
assert message["automation_id"] == "job-1"
assert message["automation_name"] == "Daily check"
assert message["automation_run_id"] == "job-1:1"
assert message["automation_prompt_ref"] == prompt_ref
def test_clean_generated_title_strips_reasoning_tags() -> None: def test_clean_generated_title_strips_reasoning_tags() -> None:
assert clean_generated_title("<think>reasoning</think> WebUI polish") == "WebUI polish" assert clean_generated_title("<think>reasoning</think> WebUI polish") == "WebUI polish"
assert clean_generated_title("Title: <think> The user said hello") == "" assert clean_generated_title("Title: <think> The user said hello") == ""

View File

@ -592,8 +592,8 @@ async def test_waiting_dispatch_does_not_replace_active_pending_queue(tmp_path):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_followup_routed_to_pending_queue(tmp_path): async def test_followup_routed_to_pending_queue(tmp_path):
"""Unified-session follow-ups should route into the active pending queue.""" """Unified-session follow-ups should route into the active pending queue."""
from nanobot.agent.loop import UNIFIED_SESSION_KEY
from nanobot.bus.events import InboundMessage from nanobot.bus.events import InboundMessage
from nanobot.session.keys import UNIFIED_SESSION_KEY
loop = _make_loop(tmp_path) loop = _make_loop(tmp_path)
loop._unified_session = True loop._unified_session = True

View File

@ -11,10 +11,10 @@ from urllib.parse import urlencode
import httpx import httpx
import pytest import pytest
from nanobot.agent.loop import UNIFIED_SESSION_KEY
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
from nanobot.cron.types import CronJob, CronPayload, CronSchedule from nanobot.cron.types import CronJob, CronPayload, CronSchedule
from nanobot.session.keys import UNIFIED_SESSION_KEY
from nanobot.session.manager import Session, SessionManager from nanobot.session.manager import Session, SessionManager
from nanobot.webui.gateway_services import GatewayServices, build_gateway_services from nanobot.webui.gateway_services import GatewayServices, build_gateway_services

View File

@ -11,11 +11,13 @@ from typer.testing import CliRunner
from nanobot.bus.events import InboundMessage, OutboundMessage from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.cli.commands import _proactive_delivery_metadata, app from nanobot.cli.commands import _proactive_delivery_metadata, app
from nanobot.config.schema import Config from nanobot.config.schema import Config
from nanobot.cron.automation import AUTOMATION_DEFER_UNTIL_IDLE_META, AUTOMATION_TRIGGER_META
from nanobot.cron.types import CronJob, CronPayload from nanobot.cron.types import CronJob, CronPayload
from nanobot.providers.factory import ProviderSnapshot, make_provider from nanobot.providers.factory import ProviderSnapshot, make_provider
from nanobot.providers.openai_codex_provider import _strip_model_prefix from nanobot.providers.openai_codex_provider import _strip_model_prefix
from nanobot.providers.registry import find_by_name from nanobot.providers.registry import find_by_name
from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY from nanobot.session.routing import SESSION_ROUTING_METADATA_KEY
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
runner = CliRunner() runner = CliRunner()
@ -23,7 +25,7 @@ runner = CliRunner()
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None: def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
metadata = { metadata = {
"webui": True, "webui": True,
"webui_turn_id": "turn-that-created-the-reminder", WEBUI_TURN_METADATA_KEY: "turn-that-created-the-reminder",
"workspace_scope": {"mode": "default"}, "workspace_scope": {"mode": "default"},
} }
@ -36,9 +38,9 @@ def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
assert out["webui"] is True assert out["webui"] is True
assert out["workspace_scope"] == {"mode": "default"} assert out["workspace_scope"] == {"mode": "default"}
assert out["webui_turn_id"].startswith("cron:drink-water:") assert out[WEBUI_TURN_METADATA_KEY].startswith("cron:drink-water:")
assert out["webui_turn_id"] != metadata["webui_turn_id"] assert out[WEBUI_TURN_METADATA_KEY] != metadata[WEBUI_TURN_METADATA_KEY]
assert out["_webui_message_source"] == {"kind": "cron", "label": "drink water"} assert out[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {"kind": "cron", "label": "drink water"}
def _fake_provider(): def _fake_provider():
@ -1350,7 +1352,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
to="chat-1", to="chat-1",
channel_meta={ channel_meta={
"webui": True, "webui": True,
"webui_turn_id": old_turn_id, WEBUI_TURN_METADATA_KEY: old_turn_id,
"workspace_scope": {"mode": "default"}, "workspace_scope": {"mode": "default"},
}, },
), ),
@ -1365,9 +1367,9 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
assert delivered.chat_id == "chat-1" assert delivered.chat_id == "chat-1"
assert delivered.metadata["webui"] is True assert delivered.metadata["webui"] is True
assert delivered.metadata["workspace_scope"] == {"mode": "default"} assert delivered.metadata["workspace_scope"] == {"mode": "default"}
assert delivered.metadata["webui_turn_id"].startswith("cron:drink-water:") assert delivered.metadata[WEBUI_TURN_METADATA_KEY].startswith("cron:drink-water:")
assert delivered.metadata["webui_turn_id"] != old_turn_id assert delivered.metadata[WEBUI_TURN_METADATA_KEY] != old_turn_id
assert delivered.metadata["_webui_message_source"] == { assert delivered.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
"kind": "cron", "kind": "cron",
"label": "drink water", "label": "drink water",
} }
@ -1653,14 +1655,17 @@ def test_gateway_bound_cron_runs_as_session_turn(
assert "Automation: Check repository health." in msg.content assert "Automation: Check repository health." in msg.content
assert msg.metadata["webui"] is True assert msg.metadata["webui"] is True
assert msg.metadata["workspace_scope"]["project_path"] == str(tmp_path) assert msg.metadata["workspace_scope"]["project_path"] == str(tmp_path)
assert msg.metadata["_webui_message_source"] == {"kind": "cron", "label": "Repo check"} assert msg.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
trigger = msg.metadata["_automation_trigger"] "kind": "cron",
"label": "Repo check",
}
trigger = msg.metadata[AUTOMATION_TRIGGER_META]
assert trigger["job_id"] == "repo-check" assert trigger["job_id"] == "repo-check"
assert trigger["job_name"] == "Repo check" assert trigger["job_name"] == "Repo check"
assert trigger["persist_content"] == ( assert trigger["persist_content"] == (
"Scheduled automation triggered: Repo check\n\nCheck repository health." "Scheduled automation triggered: Repo check\n\nCheck repository health."
) )
assert msg.metadata["_defer_until_session_idle"] is True assert msg.metadata[AUTOMATION_DEFER_UNTIL_IDLE_META] is True
statuses = [record["status"] for _run_id, record in seen["run_records"]] statuses = [record["status"] for _run_id, record in seen["run_records"]]
assert statuses == ["queued", "ok"] assert statuses == ["queued", "ok"]
assert seen["run_records"][0][0] == seen["run_records"][1][0] assert seen["run_records"][0][0] == seen["run_records"][1][0]

View File

@ -347,7 +347,7 @@ def test_add_job_requires_session_key(tmp_path) -> None:
tool = _make_tool(tmp_path) tool = _make_tool(tmp_path)
tool.set_context(RequestContext(channel="telegram", chat_id="chat-1")) tool.set_context(RequestContext(channel="telegram", chat_id="chat-1"))
result = tool._add_job(None, "Background refresh", 60, None, None, None, deliver=False) result = tool._add_job(None, "Background refresh", 60, None, None, None)
assert result == "Error: scheduled automations must be created from a chat session" assert result == "Error: scheduled automations must be created from a chat session"
assert tool._cron.list_jobs() == [] assert tool._cron.list_jobs() == []

View File

@ -10,6 +10,7 @@ from nanobot.agent.tools.cron import CronTool
from nanobot.agent.tools.message import MessageTool from nanobot.agent.tools.message import MessageTool
from nanobot.agent.tools.spawn import SpawnTool from nanobot.agent.tools.spawn import SpawnTool
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
from nanobot.session.keys import UNIFIED_SESSION_KEY
@pytest.mark.asyncio @pytest.mark.asyncio
@ -262,7 +263,7 @@ async def test_webui_cron_tool_uses_unified_session_when_enabled(tmp_path) -> No
"websocket", "websocket",
"chat-123", "chat-123",
metadata={"webui": True}, metadata={"webui": True},
session_key="unified:default", session_key=UNIFIED_SESSION_KEY,
) )
result = await tool.execute(action="add", message="standup", every_seconds=300) result = await tool.execute(action="add", message="standup", every_seconds=300)
@ -270,7 +271,7 @@ async def test_webui_cron_tool_uses_unified_session_when_enabled(tmp_path) -> No
jobs = tool._cron.list_jobs() jobs = tool._cron.list_jobs()
assert len(jobs) == 1 assert len(jobs) == 1
assert jobs[0].payload.session_key == "unified:default" assert jobs[0].payload.session_key == UNIFIED_SESSION_KEY
@pytest.mark.asyncio @pytest.mark.asyncio