The tick
backend/services/heartbeat.py has an async _tick() that runs on a schedule. Each tick:
- Skip if Dad is currently chatting (streaming guard, lesson 4).
- Run vault auto-indexing (incremental ChromaDB update for changed files).
- Read Markdown tasks under
vault/todos/. - Pass them to an LLM with the current time and recent context.
- If the LLM decides to act, execute (could be: send a Telegram message, post to cwk-site, fetch market data, anything).
- Write the judgment and execution result to the heartbeat log and the durable surface that owns the task.
Why an LLM is the right scheduler here
'Every Monday morning unless it's a holiday' is hard to express in cron. 'When the market closes red 3 days in a row' is hard to express in any DSL. An LLM reads the todo + the world state + the recent context and decides. The scheduler isn't the cron — it's the judgment.
Registry-first cron prompts
Scheduled task prompts (weather brief, daily comment check, market dispatch, etc.) live in an editable registry rather than baked into Python. Admin can rewrite a prompt and the next run picks it up without a restart. The cron snapshot in <app-data>/cron_jobs.json records which registry prompt fired and when, so the trail is debuggable from the API endpoint alone.
Keep the general heartbeat tick and scheduled cron separate. The tick is a loose judgment loop over vault todos; cron is an exact trigger registered in cron_jobs.json with a prompt-registry reference. They share the brain fallback contract and streaming guard, but not their task source or execution receipt.
Long work also does not rely on one model turn remembering everything. A scheduled conversation can bind a stateful task whose step status and artifacts live in durable storage, so the next tick or rollover resumes unfinished work instead of starting over.