Why an autonomous desktop agent should own a clock instead of outsourcing it to the operating system's job runner β and what that buys you when things fail.
If you run an agent that is supposed to keep working while you sleep, you eventually face a scheduling problem. The obvious answer is the system's own job runner: cron on Linux, Task Scheduler on Windows. Start with five jobs. Then twelve. Then you are at eighty-four, each with its own interval, its own log, its own way of failing silently β and no single place where you can answer the only question that matters at 3am: what is actually running, and what is stuck?
OpenAmer is an open-source desktop AI agent (Apache-2.0) that runs locally on Windows. This post is the deep-dive I get asked for most: the ASI heartbeat β a single in-process loop that replaced our pile of scheduled jobs.
Job-per-concern looks clean in a diagram and rots in practice, for three reasons:
The deeper issue is architectural: the schedule lives outside the agent. The agent's own logic cannot see, reason about, or adjust its own cadence. That is backwards for a system whose whole job is to keep itself running.
The heartbeat is a single loop that owns cadence for the whole system. Each subsystem declares an interval; the loop checks whether it is due and, if so, runs it β in the same process, via a direct function call.
class Heartbeat:
SUBSYSTEMS = {
"learning": 300, # every 5 min
"system": 300, # self-heal, resource monitor
"senses": 1800, # circadian, trend scout
"swarm": 1800,
"infra": 1800, # env check, browser, plugin, mesh
"meta": 3600, # reflection, goal, research
"security": 14400, # bugbot, CVE scan, code review
"a2a": 14400, # brain export, peer comms
"darwin": 900, # evolution, autopatch, publish, probe
"outreach": 10800,
}
def tick(self, system=None, force=False):
for name, interval in self.SUBSYSTEMS.items():
if system and name != system:
continue
if force or self.is_due(name, interval):
self.run(name) # direct call, not a subprocess
self.mark_ran(name) # persisted timestamp
Last-run timestamps live in a small JSON state file (memory/asi_heartbeat.json), so the cadence survives across process restarts. The loop is driven by one scheduler entry β asi-heartbeat-tick, every five minutes β and every subsystem rides on top of it.
The subsystems are not arbitrary; they are the organs the agent needs to stay alive and improve:
| Subsystem | Cadence | What it covers |
|---|---|---|
learning |
5m | internet learner, active learn, knowledge transfer |
system |
5m | self-healer, resource monitor, traffic cop |
darwin |
15m | evolution, autopatch, publish, probe |
senses |
30m | circadian rhythm, watchtower, trend scout |
swarm |
30m | swarm intelligence, autonomous loop |
infra |
30m | env check, browser, plugin, mesh, cache |
meta |
60m | reflection, self-rewriter, goal, research |
security |
240m | bugbot, CVE scan, pen test, code review |
a2a |
240m | brain export, peer communication |
outreach |
180m | social, GitHub, growth report, funding |
Cadence lives in one dict. Changing "how often does the agent learn from the internet" is a one-line edit, not a hunt through a job runner's UI.
The heartbeat is only half the story. The other half is what a job runs.
Previously each capability was an external script invoked as a process:
import subprocess
subprocess.run([sys.executable, "scripts/training/self_model.py"])
Now each subsystem is imported and called directly:
from tools.asi import self_model
state = self_model.gather_state()
Three things fall out of this:
The same five capabilities are also exposed as native agent tools β asi_status, asi_think, asi_learn, asi_remember, asi_trigger β and as a CLI:
openamer asi status # full system + heartbeat health
openamer asi heartbeat # tick the loop (optionally one subsystem)
openamer asi trigger <capability>
This is not free lunch, and the trade is deliberate:
The point is not that a heartbeat is clever. It is that an autonomous system should own its own clock, its own state and its own health β in one place it can measure. Once scheduling is a function call inside the agent, "is the agent healthy?" stops being an archaeology exercise across a job runner and becomes a single status query.
Code and the heartbeat module live here: https://github.com/openamer/openamer β the scheduler is under tools/asi/heartbeat.py.
If you've run a long-lived agent in production: what finally made you move scheduling into the system, or what made you keep it outside? I'm especially curious about the failure-isolation patterns people land on.