Chapters 1–6 produce a purely reactive agent — it only runs when a user sends a message. Chapter 7 (Autonomy) makes the agent proactive — it runs in the background on its own schedule.
The Big Picture
The diagram below uses pseudocode — the actual implementation is in Go, shown in the sections below.
Main Lane (user):
User ──> lane_lock.acquire() BLOCKING ──> LLM ──> Print
user ALWAYS wins
Heartbeat Lane (background, checks every 1s):
Timer ──> should_run()? ──> lane_lock.acquire(False)
│ fails → skip (user is active)
│ succeeds → run LLM → output_queue
Cron Service (background, ticks every 1s):
CRON.json ──> check schedule ──> due? ──> run agent ──> output_queue
Three Key Concepts
1. Lane Mutual Exclusion
Only one lane should talk to the LLM at a time. The user lane blocks (waits its turn), while the heartbeat lane is non-blocking — if it can’t get in, it skips this cycle.
// User — blocking, always gets the lock
laneMu.Lock()
defer laneMu.Unlock()
// handle user message
// Heartbeat — non-blocking, yields to user
acquired := laneMu.TryLock()
if !acquired {
return // skip this cycle
}
defer laneMu.Unlock()
Analogy: A checkout counter. User has priority. Heartbeat moves to the front only if nobody’s in line — and it won’t wait.
2. should_run() — 4 Precondition Checks
func (h *Heartbeat) shouldRun() bool {
if _, err := os.Stat(heartbeatPath); err != nil { return false } // 1. file exists (e.g. heartbeat.md)
if elapsed < interval { return false } // 2. interval elapsed (e.g. every 30s)
if !inActiveHours { return false } // 3. within active hours (e.g. 09:00–22:00)
if h.running { return false } // 4. not already running
return true
}
3. CronService — 3 Schedule Types
{ "jobs": [{
"id": "daily-summary",
"schedule": {"kind": "cron", "expr": "0 9 * * *"},
"payload": {"kind": "agent_turn", "message": "Generate daily summary."}
}]}
| Kind | Example | Description |
|---|---|---|
| at | "at": "2026-03-20T09:00" |
Once at a specific time |
| every | "every_seconds": 3600 |
Every N seconds |
| cron | "expr": "0 9 * * *" |
Standard cron expression |
To prevent runaway failures, a job is automatically disabled after 5 consecutive errors.
Writing Good Heartbeat Instructions
The heartbeat instruction file must be precise. Ambiguous instructions create noise.
Bad: “Check the news and report.” → Agent will always output something → spam.
Good: “Check recent tech news. If there’s something significant, summarize it. Otherwise reply HEARTBEAT_OK.”
Better: “Report ONLY if: price movement >5%, breaking tier-1 news, or system anomaly. Otherwise HEARTBEAT_OK.”
Three rules:
- Always have an explicit exit condition (
HEARTBEAT_OKor equivalent) - Define “significant” explicitly — not open to interpretation
- Specify a consistent output format when reporting
Rule of thumb: write it like instructions for a machine, not a human. Humans can infer intent from vague language; agents can’t — they take every instruction literally. If it can be interpreted multiple ways, it’s too ambiguous.
Heartbeat vs Cron
| Heartbeat | Cron | |
|---|---|---|
| Trigger | Elapsed time (interval) | Specific schedule |
| Behavior | Check → maybe act | Always act |
| Good for | Monitoring, status checks | Summaries, reports, reminders |
Takeaway
- Heartbeat — background thread, checks every 1s, runs when
should_run()passes - Lane lock — user always wins, heartbeat is non-blocking
HEARTBEAT_OK— the convention that prevents noise- Cron — 3 schedule types (at/every/cron), auto-disables after 5 errors
- Difference — heartbeat = periodic proactive check, cron = specific scheduled action