When a cron job triggers an agent that calls back into cron (e.g.
cron.list), the non-reentrant async mutex in locked() causes a
circular wait: onTimer holds the lock waiting for the agent, the
agent's cron.list waits for the lock.
Fix by:
1. Splitting onTimer to collect due jobs under the lock, then
execute them after releasing it. The runningAtMs sentinel
prevents double-runs.
2. Adding a module-level CronService registry so the cron tool
can call the service directly in-process, bypassing the
gateway WebSocket round-trip entirely.
AI-assisted (Claude). Tested locally with cron jobs that call
cron.list mid-execution — previously deadlocked, now works.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>