From 019d4c1c3f0d9ac6e8223994c1b8ed5092900508 Mon Sep 17 00:00:00 2001 From: Teknium Date: Sun, 26 Apr 2026 07:16:27 -0700 Subject: [PATCH] feat(curator): hook into the gateway's cron-ticker thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-running gateways need the curator to fire on cadence without restarts. Piggy-back on the existing cron ticker thread (which already runs image/document cache cleanup every hour on the same pattern) instead of spawning a dedicated timer thread. - New CURATOR_EVERY = 60 ticks (poll hourly at default 60s interval). The inner config.interval_hours gate controls the real cadence, so 60 of these 60 hourly pokes are cheap no-ops and one runs the review. - Removed the boot-time call added in the prior commit — the ticker covers boot + every hour thereafter. Avoids double-running. Handles the weekly-default-on-24/7-gateway gap flagged in review. --- gateway/run.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index a7fa0a816..7cf2d5901 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2383,29 +2383,6 @@ class GatewayRunner: # Discover and load event hooks self.hooks.discover_and_load() - # Curator — kick off a background skill-maintenance pass on gateway - # startup if the schedule says we're due. Runs in a daemon thread - # so it never blocks gateway startup. Best-effort; any failure is - # swallowed. The interval_hours gate prevents re-running on quick - # restarts. - try: - from agent.curator import maybe_run_curator - - def _curator_summary(msg: str) -> None: - # Surface the one-line summary into gateway logs so operators - # can see what the curator did. No per-platform push since - # there's no user-facing session at gateway boot. - logger.info("curator: %s", msg) - - maybe_run_curator( - idle_for_seconds=float("inf"), # gateway boot = no active agent - on_summary=_curator_summary, - ) - except Exception: - logger.debug( - "curator boot hook failed", exc_info=True, - ) - # Recover background processes from checkpoint (crash recovery) try: @@ -11767,6 +11744,7 @@ def _start_cron_ticker(stop_event: threading.Event, adapters=None, loop=None, in IMAGE_CACHE_EVERY = 60 # ticks — once per hour at default 60s interval CHANNEL_DIR_EVERY = 5 # ticks — every 5 minutes PASTE_SWEEP_EVERY = 60 # ticks — once per hour + CURATOR_EVERY = 60 # ticks — poll hourly (inner gate handles the real cadence) logger.info("Cron ticker started (interval=%ds)", interval) tick_count = 0 @@ -11818,6 +11796,21 @@ def _start_cron_ticker(stop_event: threading.Event, adapters=None, loop=None, in except Exception as e: logger.debug("Paste sweep error: %s", e) + # Curator — piggy-back on the existing cron ticker so long-running + # gateways get weekly skill maintenance without needing restarts. + # maybe_run_curator() is internally gated by config.interval_hours + # (7 days by default), so CURATOR_EVERY is just the poll rate — the + # real work only fires once per config interval. + if tick_count % CURATOR_EVERY == 0: + try: + from agent.curator import maybe_run_curator + maybe_run_curator( + idle_for_seconds=float("inf"), + on_summary=lambda msg: logger.info("curator: %s", msg), + ) + except Exception as e: + logger.debug("Curator tick error: %s", e) + stop_event.wait(timeout=interval) logger.info("Cron ticker stopped")