diff --git a/nanobot/cli/gateway_runtime.py b/nanobot/cli/gateway_runtime.py index c5feee52e..b65fbdbb6 100644 --- a/nanobot/cli/gateway_runtime.py +++ b/nanobot/cli/gateway_runtime.py @@ -785,8 +785,10 @@ def _run_gateway( console.print(f"[green]✓[/green] Dream: {dream_cfg.describe_schedule()}") else: console.print("[yellow]○[/yellow] Dream: disabled") - cron.remove_system_job("dream") + # Advance the cursor first: it must happen unconditionally (issue + # #4242), even if the cron store turns out to be corrupt below. _advance_dream_cursor_if_behind(agent.context.memory) + cron.remove_system_job("dream") # Register Heartbeat system job (idempotent on restart) if hb_cfg.enabled: diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index 228b4c4f4..7ae494d7e 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -3221,7 +3221,8 @@ def test_gateway_local_trigger_queue_submits_agent_turns( def register_system_job(self, _job) -> None: return None - def remove_system_job(self, _job_id: str) -> bool: + def remove_system_job(self, job_id: str) -> bool: + seen.setdefault("removed_system_jobs", []).append(job_id) return False class _FakeAgentLoop(_GatewayAgentContractStub): @@ -3300,6 +3301,8 @@ def test_gateway_local_trigger_queue_submits_agent_turns( turn_delivery_factory = agent_kwargs["turn_delivery_factory"] assert isinstance(turn_delivery_factory, TurnDeliveryFactory) assert turn_delivery_factory.bus is bus + # Disabled system jobs must be retired on startup, not just unregistered. + assert seen["removed_system_jobs"] == ["dream", "heartbeat"] assert isinstance(turn_delivery_factory.route_policy, WebuiTurnRoutePolicy) assert turn_delivery_factory.route_policy.sessions is agent.sessions diff --git a/tests/cron/test_cron_service.py b/tests/cron/test_cron_service.py index 3653ae6da..fa1c3fb32 100644 --- a/tests/cron/test_cron_service.py +++ b/tests/cron/test_cron_service.py @@ -818,6 +818,15 @@ def test_remove_system_job_retires_persisted_system_job(tmp_path) -> None: assert other.remove_job("dream") == "protected" +def test_remove_system_job_without_store_file(tmp_path) -> None: + """Fresh install with the system job disabled: no jobs.json exists yet.""" + store_path = tmp_path / "cron" / "jobs.json" + service = CronService(store_path) + + assert service.remove_system_job("heartbeat") is False + assert not store_path.exists() + + @pytest.mark.asyncio async def test_start_server_not_jobs(tmp_path): store_path = tmp_path / "cron" / "jobs.json"