feat: cron job system for Inara (remind + note types)
- cron_runner.py: job storage (CRONS.json), schedule parsing, execution - tools/cron.py: cron_list/add/remove/toggle + reminders_clear tools - scheduler.py: load user crons at startup, expose get_scheduler() for live add/remove without restarts - context_loader.py: auto-include REMINDERS.md in system prompt (tier 2+) so cron reminders surface automatically without Inara having to poll - inara/CRONS.json + REMINDERS.md: backing files (initially empty) Schedule formats: hourly | daily | daily:HH:MM | weekly:DOW | weekly:DOW:HH:MM Job types: remind (→ REMINDERS.md) | note (→ SCRATCH.md) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,13 +52,17 @@ async def _run_long() -> None:
|
||||
logger.error("auto distill long failed: %s", e)
|
||||
|
||||
|
||||
def get_scheduler() -> AsyncIOScheduler | None:
|
||||
"""Return the running scheduler instance (used by cron tools for live add/remove)."""
|
||||
return _scheduler
|
||||
|
||||
|
||||
def start() -> None:
|
||||
global _scheduler
|
||||
_scheduler = AsyncIOScheduler(timezone=ZoneInfo(settings.scheduler_timezone))
|
||||
|
||||
if not settings.auto_distill:
|
||||
logger.info("auto distillation disabled (AUTO_DISTILL=false)")
|
||||
return
|
||||
|
||||
_scheduler = AsyncIOScheduler(timezone=ZoneInfo(settings.scheduler_timezone))
|
||||
|
||||
if settings.auto_distill_short:
|
||||
_scheduler.add_job(_run_short, "cron", hour=3, minute=0, id="distill_short")
|
||||
@@ -72,11 +76,42 @@ def start() -> None:
|
||||
_scheduler.add_job(_run_long, "cron", day=1, hour=4, minute=0, id="distill_long")
|
||||
logger.info("scheduled: distill_long monthly on 1st at 04:00")
|
||||
|
||||
if _scheduler.get_jobs():
|
||||
_scheduler.start()
|
||||
logger.info("auto distillation scheduler started (%d jobs)", len(_scheduler.get_jobs()))
|
||||
else:
|
||||
logger.info("auto distillation: no jobs enabled")
|
||||
# Load user-defined cron jobs from CRONS.json
|
||||
_load_user_crons()
|
||||
|
||||
_scheduler.start()
|
||||
logger.info("scheduler started (%d jobs)", len(_scheduler.get_jobs()))
|
||||
|
||||
|
||||
def _load_user_crons() -> None:
|
||||
"""Register all enabled user-defined cron jobs from CRONS.json."""
|
||||
import asyncio
|
||||
try:
|
||||
from cron_runner import load_crons, parse_schedule, run_job
|
||||
except ImportError as e:
|
||||
logger.warning("could not import cron_runner: %s", e)
|
||||
return
|
||||
|
||||
crons = load_crons()
|
||||
loaded = 0
|
||||
for job in crons:
|
||||
if not job.get("enabled", True):
|
||||
continue
|
||||
try:
|
||||
kwargs = parse_schedule(job["schedule"])
|
||||
_scheduler.add_job(
|
||||
lambda j=job: asyncio.ensure_future(run_job(j)),
|
||||
"cron",
|
||||
id=job["id"],
|
||||
replace_existing=True,
|
||||
**kwargs,
|
||||
)
|
||||
loaded += 1
|
||||
except Exception as e:
|
||||
logger.warning("cron job %s skipped: %s", job.get("id"), e)
|
||||
|
||||
if loaded:
|
||||
logger.info("loaded %d user cron job(s)", loaded)
|
||||
|
||||
|
||||
def stop() -> None:
|
||||
|
||||
Reference in New Issue
Block a user