Skip to content

Fix cross-agent cron leakage from global store into per-agent cron files #521

@cpfiffer

Description

@cpfiffer

Summary

In multi-agent mode, a cron job intended for one agent can be written to the global cron store, then later migrated into a different agent's per-agent cron file and executed there.

I hit this with a signal Morning Report job that should have belonged to co-3, but it was migrated into central and executed by central. Since central only had Telegram configured, delivery failed with Channel not found: signal.

Observed behavior

  • co-3 has Signal configured
  • central only has Telegram configured
  • A Morning Report job with deliver: signal:... ended up in /tmp/lettabot/cron-jobs-central.json
  • Cron ran the job under central
  • Delivery failed:
    • job_deliver_failed ... error: "Channel not found: signal"

Expected behavior

  • A cron job created for one agent should never leak into another agent's cron store
  • An agent should only execute cron jobs scoped to its own store/config
  • Global store migration should not cause cross-agent job adoption

Evidence

Config:

  • lettabot.yaml: central has Telegram only; co-3 has Signal

Runtime files:

  • /tmp/lettabot/cron-jobs-co-3.json contains the expected co-3 Morning Report
  • /tmp/lettabot/cron-jobs-central.json contained a separate Signal-targeted Morning Report:
    • id: cron-1772751695606-j44y8f
    • deliver.channel: signal

Logs from /tmp/lettabot/cron-log.jsonl:

  • 2026-03-05T23:01:35Z: job_created for cron-1772751695606-j44y8f
  • 2026-03-06T01:03:45Z: store_migrated_from_global from /tmp/lettabot/cron-jobs.json to /tmp/lettabot/cron-jobs-central.json
  • 2026-03-07T00:00:35Z: job_deliver_failed with Channel not found: signal

Likely root cause

lettabot-schedule still falls back to the global store when CRON_STORE_PATH is not set:

  • src/cron/cli.ts
    • const STORE_PATH = process.env.CRON_STORE_PATH || getCronStorePath();

Then CronService migrates that global store into a per-agent store on startup:

  • src/cron/service.ts
    • migrateFromGlobalStoreIfNeeded()

That allows a job written without correct per-agent scoping to be silently adopted by whichever agent first migrates the global file.

Additional issue in same area

CronService's directory-watch fallback is still hardcoded to cron-jobs.json:

  • src/cron/service.ts
    • if (filename === 'cron-jobs.json') { ... }

In multi-agent mode this looks wrong for per-agent files like cron-jobs-co-3.json and cron-jobs-central.json.

Suggested fix

  • Make cron job creation fail closed in multi-agent mode unless the per-agent cron store path is explicit
  • Avoid migrating the global cron store into per-agent stores once multi-agent mode is active, or gate that migration much more tightly
  • Update the file watcher to watch the actual basename of this.storePath, not a hardcoded cron-jobs.json
  • Consider validating deliver.channel against the channels configured for the agent that owns the cron store

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions