Skip to content

heartbeat: a memoryless agent on a timer

The problem

You want an agent to work on something big while you are away. One long session drifts, runs out of context, or dies halfway with nothing written down. Running it on a schedule is easy; the hard part is that every run starts blank, so it redoes work, wanders off, or trips over the run before it.

The method

Treat the blank start as the design. The agent's whole memory is a folder it reads first and writes last:

TASK.md       the goal and the boundaries (the human writes this)
STATE.md      done / next / blockers (the agent keeps it true)
PROTOCOL.md   on waking: read, pick ONE chunk, do it, record, exit
journal/      one note per wake: Did, Learned, Next, Left open
work/         the output

The runner stays thin and does only what the agent should not think about:

  • Gate before counting. Any command; non-zero skips the wake without using up the cap. Gate on battery, load, a usage budget, office hours.
  • Cap. Stop after N wakes, so a forgotten timer cannot run forever.
  • No overlap. A slow wake makes the next tick skip, never run twice.
  • Distrust a quiet success. An agent can exit 0 having done nothing; the log says so.
  • Tell it what it cannot know. The prompt carries the time (for the journal file name), whether the folder is shared, and the full path of the claim command. A solo agent told nothing tried to claim anyway and stalled on a tool it could not run.

Locks are mkdir, which is atomic: exactly one caller makes the directory. The same trick lets two agents share one folder without doing the same chunk. Because the brain is a Markdown file, you retune the agent by editing PROTOCOL.md, not code.

Sample

The lock, in POSIX sh:

take() {  # take LOCKDIR ID
  if mkdir "$1" 2>/dev/null; then echo "$2 $(date +%s)" >"$1/owner"; return 0; fi
  read -r _ when <"$1/owner" 2>/dev/null || return 1     # held, owner mid-write
  [ $(( $(date +%s) - when )) -gt "$STALE" ] || return 1 # held, fresh
  mv "$1" "$1.stale.$$" || return 1    # rename is atomic: one taker gets past
  rm -rf "$1.stale.$$"; mkdir "$1" || return 1
  echo "$2 $(date +%s)" >"$1/owner"
}

Use it:

heartbeat init ~/jobs/docs          # then edit TASK.md
heartbeat wake ~/jobs/docs          # one wake by hand
heartbeat schedule ~/jobs/docs 15   # launchd plist or systemd timer

Any agent CLI works: set HEARTBEAT_AGENT to a command that takes the wake prompt as $1. Set HEARTBEAT_SHARED=1 when several agents share a folder.

Limits

  • The agent must keep STATE.md honest; the journal is the backstop.
  • One wake does one chunk. Size chunks in TASK.md.
  • Claims stop honest agents duplicating work. They are not a security boundary.
  • The cap stops wakes, not the timer; unload it when done.

Code

methods/heartbeat/ in the methods repository: the script, a README and a test that drives it with a stand-in agent.