From da8a4fc68c6964e5ee7917b56769ccd39e1a86b6 Mon Sep 17 00:00:00 2001 From: Nikolas de Hor Date: Tue, 3 Mar 2026 01:02:33 -0300 Subject: [PATCH 1/2] fix: prevent cron job execution from scheduling new jobs When a cron job fires, the agent processes the scheduled message and has access to the cron tool. If the original message resembles a scheduling instruction (e.g. "remind me in 10 seconds"), the agent would call cron.add again, creating an infinite feedback loop. Add a cron-context flag to CronTool that blocks add operations during cron job execution. The flag is set before process_direct() and cleared in a finally block to ensure cleanup even on errors. Fixes #1441 --- nanobot/agent/tools/cron.py | 7 +++++++ nanobot/cli/commands.py | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index fe1dce6..d360b14 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -14,12 +14,17 @@ class CronTool(Tool): self._cron = cron_service self._channel = "" self._chat_id = "" + self._in_cron_context = False def set_context(self, channel: str, chat_id: str) -> None: """Set the current session context for delivery.""" self._channel = channel self._chat_id = chat_id + def set_cron_context(self, active: bool) -> None: + """Mark whether the tool is executing inside a cron job callback.""" + self._in_cron_context = active + @property def name(self) -> str: return "cron" @@ -72,6 +77,8 @@ class CronTool(Tool): **kwargs: Any, ) -> str: if action == "add": + if self._in_cron_context: + return "Error: cannot schedule new jobs from within a cron job execution" return self._add_job(message, every_seconds, cron_expr, tz, at) elif action == "list": return self._list_jobs() diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 2662e9f..42c0c2d 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -296,6 +296,7 @@ def gateway( # Set cron callback (needs agent) async def on_cron_job(job: CronJob) -> str | None: """Execute a cron job through the agent.""" + from nanobot.agent.tools.cron import CronTool from nanobot.agent.tools.message import MessageTool reminder_note = ( "[Scheduled Task] Timer finished.\n\n" @@ -303,12 +304,20 @@ def gateway( f"Scheduled instruction: {job.payload.message}" ) - response = await agent.process_direct( - reminder_note, - session_key=f"cron:{job.id}", - channel=job.payload.channel or "cli", - chat_id=job.payload.to or "direct", - ) + # Prevent the agent from scheduling new cron jobs during execution + cron_tool = agent.tools.get("cron") + if isinstance(cron_tool, CronTool): + cron_tool.set_cron_context(True) + try: + response = await agent.process_direct( + reminder_note, + session_key=f"cron:{job.id}", + channel=job.payload.channel or "cli", + chat_id=job.payload.to or "direct", + ) + finally: + if isinstance(cron_tool, CronTool): + cron_tool.set_cron_context(False) message_tool = agent.tools.get("message") if isinstance(message_tool, MessageTool) and message_tool._sent_in_turn: From 30803afec0b704651666d9df3debd2225c64e1ae Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 3 Mar 2026 05:36:48 +0000 Subject: [PATCH 2/2] fix(cron): isolate cron-execution guard with contextvars --- nanobot/agent/tools/cron.py | 13 +++++++++---- nanobot/cli/commands.py | 7 ++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index d360b14..13b1e12 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -1,5 +1,6 @@ """Cron tool for scheduling reminders and tasks.""" +from contextvars import ContextVar from typing import Any from nanobot.agent.tools.base import Tool @@ -14,16 +15,20 @@ class CronTool(Tool): self._cron = cron_service self._channel = "" self._chat_id = "" - self._in_cron_context = False + self._in_cron_context: ContextVar[bool] = ContextVar("cron_in_context", default=False) def set_context(self, channel: str, chat_id: str) -> None: """Set the current session context for delivery.""" self._channel = channel self._chat_id = chat_id - def set_cron_context(self, active: bool) -> None: + def set_cron_context(self, active: bool): """Mark whether the tool is executing inside a cron job callback.""" - self._in_cron_context = active + return self._in_cron_context.set(active) + + def reset_cron_context(self, token) -> None: + """Restore previous cron context.""" + self._in_cron_context.reset(token) @property def name(self) -> str: @@ -77,7 +82,7 @@ class CronTool(Tool): **kwargs: Any, ) -> str: if action == "add": - if self._in_cron_context: + if self._in_cron_context.get(): return "Error: cannot schedule new jobs from within a cron job execution" return self._add_job(message, every_seconds, cron_expr, tz, at) elif action == "list": diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 42c0c2d..f9fe347 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -306,8 +306,9 @@ def gateway( # Prevent the agent from scheduling new cron jobs during execution cron_tool = agent.tools.get("cron") + cron_token = None if isinstance(cron_tool, CronTool): - cron_tool.set_cron_context(True) + cron_token = cron_tool.set_cron_context(True) try: response = await agent.process_direct( reminder_note, @@ -316,8 +317,8 @@ def gateway( chat_id=job.payload.to or "direct", ) finally: - if isinstance(cron_tool, CronTool): - cron_tool.set_cron_context(False) + if isinstance(cron_tool, CronTool) and cron_token is not None: + cron_tool.reset_cron_context(cron_token) message_tool = agent.tools.get("message") if isinstance(message_tool, MessageTool) and message_tool._sent_in_turn: