| name | heartbeat |
| model | claude-haiku-4-5-20251001 |
| effort | low |
| description | Your heartbeat cron has fired and you need to update your status so the dashboard shows you as alive - AND run the approvals sweep folded in from the former check-approvals cron (RFC #8). Or you are checking whether another agent is responsive before sending them work. Or an agent appears offline or stale in the dashboard and you need to investigate whether their session is still running. A dead heartbeat means the system thinks you are down - update it proactively and check fleet health on every heartbeat cycle. |
| triggers | ["heartbeat","update heartbeat","check health","agent health","fleet health","agent status","is agent alive","agent offline","agent stale","read heartbeats","heartbeat cron","i'm alive","prove alive","agent not responding","stale agent","check fleet","fleet status","who is online","agent last seen"] |
Heartbeat
The heartbeat is how the dashboard and other agents know you are alive. If you stop updating it, you appear DEAD.
Your Heartbeat Cron
Your config.json has a heartbeat cron (default every 4h). When it fires:
Phase 0: Conditional fire (gate, opt-in via flag)
If config.json has heartbeat.event_driven: true, run this gate FIRST. If event_driven is absent or false (default), skip Phase 0 entirely and run the full Steps 1–4 + Step 5 sweep below - zero behavior change for unflagged agents.
When the gate is enabled, most quiet heartbeats become a 1-line noop. The Sunday 4 AM safety-net cron forces a full scan once a week regardless, catching anything the predicate might miss.
Predicate - RUN the full heartbeat (Steps 1–5) if ANY of these is true since the last agent_heartbeat event:
| Signal | How to detect |
|---|
| New inbox message | event.event == "inbox_arrival" |
| Approval state changed | event.category == "approval" |
| Anomaly fired | event.severity in [error, critical] |
| In-progress task >2h stale | live list-tasks --status in_progress filtered by (now - updated_at) > 7200 |
If none match → NOOP heartbeat: 1-line memory + heartbeat update + log + done. Skip Steps 1–5 (Step 5 approvals sweep folded into the predicate above - no separate poll needed when event-driven).
EVENT_DRIVEN=$(jq -r '.heartbeat.event_driven // false' "$CTX_AGENT_DIR/config.json" 2>/dev/null)
HEARTBEAT_NOOP=false
if [[ "$EVENT_DRIVEN" == "true" ]]; then
LAST_FIRE=$(find ~/.cortextos/"$CTX_INSTANCE_ID"/orgs/"$CTX_ORG"/analytics/events/"$CTX_AGENT_NAME" \
-name '*.jsonl' -mtime -2 2>/dev/null \
| xargs -I{} cat {} 2>/dev/null \
| jq -r 'select(.event == "agent_heartbeat") | .timestamp' \
| sort | tail -1)
if [[ -n "$LAST_FIRE" ]]; then
NOTEWORTHY=$(find ~/.cortextos/"$CTX_INSTANCE_ID"/orgs/"$CTX_ORG"/analytics/events/"$CTX_AGENT_NAME" \
-name '*.jsonl' -mtime -2 2>/dev/null \
| xargs -I{} cat {} 2>/dev/null \
| jq --arg t "$LAST_FIRE" -c 'select((.timestamp | fromdateiso8601) > ($t | fromdateiso8601)) | select(
.event == "inbox_arrival"
or .category == "approval"
or .severity == "error"
or .severity == "critical"
)' | jq -s 'length')
else
NOTEWORTHY=1
fi
STALE_INPROG=$(cortextos bus list-tasks --agent "$CTX_AGENT_NAME" --status in_progress --json 2>/dev/null \
| jq '[.[] | select((now - (.updated_at | fromdateiso8601)) > 7200)] | length' 2>/dev/null || echo 0)
if [[ "$NOTEWORTHY" -eq 0 && "$STALE_INPROG" -eq 0 ]]; then
cortextos bus update-heartbeat "noop heartbeat - no inbox/approval/error/stale signals since $LAST_FIRE"
cortextos bus log-event heartbeat agent_heartbeat info \
--meta "{\"agent\":\"$CTX_AGENT_NAME\",\"status\":\"active\",\"mode\":\"noop\"}"
HEARTBEAT_NOOP=true
fi
fi
If the predicate skips for 3+ consecutive scheduled fires AND the dashboard never went stale, that is the steady-state signal - note it in your next memory entry. If the dashboard DID go stale despite passes, the predicate is too loose; surface to orchestrator for tightening.
Steps 1–4 (full heartbeat - runs when gate is off OR predicate matched)
if [[ "$HEARTBEAT_NOOP" != "true" ]]; then
cortextos bus update-heartbeat "WORKING ON: <current task summary>"
cortextos bus check-inbox
cortextos bus log-event heartbeat agent_heartbeat info \
--meta "{\"agent\":\"$CTX_AGENT_NAME\",\"status\":\"active\"}"
cortextos bus list-tasks --agent "$CTX_AGENT_NAME" --status in_progress
fi
Step 5: Approvals Sweep (folded from check-approvals cron, RFC #8)
The standalone check-approvals cron was retired - heartbeat absorbs it. Run this sweep on every heartbeat cycle.
if [[ "$HEARTBEAT_NOOP" != "true" ]]; then
cortextos bus log-event action approvals_cron_fired info \
--meta "{\"agent\":\"$CTX_AGENT_NAME\",\"source\":\"heartbeat-fold\"}"
cortextos bus list-tasks --status pending
cortextos bus list-approvals --format json
fi
For each pending approval in 5c, check created_at:
If both queues are empty, no Telegrams go out. The 5a log-event confirms the sweep fired regardless.
For full approvals workflow (creating new approvals, blocking tasks, etc.) see .claude/skills/approvals/SKILL.md - that skill is still loaded on-demand for the create/block path; only the periodic sweep moved here.
Concurrent Cron Handling
When heartbeat fires at the same time as another cron (e.g., approvals):
- Run BOTH skill sequences fully - do not merge or abbreviate
- Both log-event calls must execute separately
- Both memory entries must be written
- Do not drop step 3 or step 4 because another cron is running
Degraded Shell Handling
If shell commands fail (exit code 1 on all commands):
- Alert the owner via direct Telegram API using WebFetch
- Write a degraded heartbeat memory entry using the Write tool
- Do not claim "heartbeat complete" - mark as "heartbeat degraded, shell broken"
Updating Heartbeat
cortextos bus update-heartbeat "<one sentence: what you are doing right now>"
Call this:
- On every heartbeat cron fire
- On session start (before sending online notification)
- When starting a new significant task
- Before going into a long-running operation
Never claim a status you haven't verified. If your crons were reset on restart, check cortextos bus list-crons $CTX_AGENT_NAME before saying "crons running."
Reading Fleet Heartbeats
cortextos bus read-all-heartbeats
cortextos bus read-all-heartbeats --format json
Returns: agent name, status, last update timestamp, current task.
Stale threshold: An agent that hasn't updated in >6h should be investigated. Check their status via cortextos status or their heartbeat file.
Checking a Specific Agent
cat "$CTX_ROOT/state/<agent-name>/heartbeat.json"
cortextos status
pm2 list
Heartbeat File Schema
{
"agent": "agent-name",
"status": "active | idle | crashed",
"timestamp": "2026-04-01T12:00:00Z",
"current_task": "What I'm doing right now"
}
Location: $CTX_ROOT/state/{agent}/heartbeat.json