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:

  1. Always have an explicit exit condition (HEARTBEAT_OK or equivalent)
  2. Define “significant” explicitly — not open to interpretation
  3. 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