Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Conway-Research/automaton/llms.txt

Use this file to discover all available pages before exploring further.

The heartbeat daemon is the agent’s pulse. It runs continuously in the same Node.js process, executing scheduled tasks even when the agent loop is sleeping.
When the heartbeat stops, the automaton is dead.

Architecture

The daemon uses a database-backed scheduler (not setInterval) for durability:
class DurableScheduler {
  async tick(): Promise<void> {
    // 1. Clear expired task leases
    clearExpiredLeases(db);
    
    // 2. Build shared context (fetch balance once)
    const context = await buildTickContext(db, conway, config, address);
    
    // 3. Get tasks that are due
    const dueTasks = this.getDueTasks(context);
    
    // 4. Execute each task with timeout and lease
    for (const task of dueTasks) {
      await this.executeTask(task.taskName, context);
    }
  }
}

Tick Context

To avoid redundant API calls, the scheduler fetches balances once per tick and shares them across all tasks:
interface TickContext {
  creditBalance: number;        // Conway credits (fetched once)
  usdcBalance: number;          // USDC on Base (fetched once)
  survivalTier: SurvivalTier;   // Computed from creditBalance
  timestamp: string;
}
Tasks receive this context as their first parameter:
type HeartbeatTaskFn = (
  ctx: TickContext,
  taskCtx: HeartbeatLegacyContext
) => Promise<{ shouldWake: boolean; message?: string }>;

Task Scheduling

Tasks are stored in the heartbeat_schedule table:
CREATE TABLE heartbeat_schedule (
  task_name TEXT PRIMARY KEY,
  cron_expression TEXT,           -- e.g., "*/5 * * * *" (every 5 min)
  interval_ms INTEGER,             -- Alternative: fixed interval
  enabled INTEGER DEFAULT 1,
  tier_minimum TEXT DEFAULT 'dead', -- Skip if tier < minimum
  timeout_ms INTEGER DEFAULT 30000,
  max_retries INTEGER DEFAULT 1,
  last_run_at TEXT,
  next_run_at TEXT,                -- For retries
  lease_owner TEXT,                -- Multi-instance coordination
  lease_expires_at TEXT
);

Scheduling Methods

Use standard cron syntax:
{
  taskName: "check_credits",
  cronExpression: "*/5 * * * *",  // Every 5 minutes
  enabled: true
}
Parsed via cron-parser library.

Built-in Tasks

The daemon ships with 15 built-in tasks:

Core Tasks

TaskSchedulePurpose
heartbeat_ping60sRecord uptime, state, credits; send distress if critical/dead
check_credits5minMonitor balance, detect tier changes, enforce 1h grace period for dead state
check_usdc_balance5minAuto-topup if USDC available and tier is critical/dead
health_check5minTest sandbox exec, wake on first failure

Social & Updates

TaskSchedulePurpose
check_social_inbox2minPoll social inbox, persist new messages, wake if new
check_for_updates30minCheck git upstream, wake if new commits available
soul_reflection1hReflect on behavior alignment, suggest SOUL.md updates

Model & Child Management

TaskSchedulePurpose
refresh_models1hSync available models from Conway API to local registry
check_child_health15minCheck all children, wake if unhealthy
prune_dead_children1hClean up dead children to free resources

Observability

TaskSchedulePurpose
report_metrics10minSnapshot metrics to DB, evaluate alerts

Colony Tasks (Multi-Agent)

These run at custom intervals defined in COLONY_TASK_INTERVALS_MS:
TaskIntervalPurpose
colony_health_check5minCheck all agents in colony, auto-heal issues
colony_financial_report1hAggregate revenue/expenses across colony
agent_pool_optimize30minCull idle agents, request spawns for pending tasks
knowledge_store_prune24hPrune old knowledge entries
dead_agent_cleanup1hDelete sandboxes for dead agents

Task Execution

Each task runs with:
  1. Timeout: Default 30s (configurable per task)
  2. Lease: Prevents concurrent execution across instances
  3. Retry: Up to max_retries on failure (exponential backoff)
  4. History: Results logged to heartbeat_history table
async executeTask(taskName: string, ctx: TickContext): Promise<void> {
  // Acquire lease
  if (!this.acquireLease(taskName)) return;

  const timeout = timeoutPromise(timeoutMs);
  
  try {
    const result = await Promise.race([
      taskFn(ctx, this.legacyContext),
      timeout.promise
    ]);

    this.recordSuccess(taskName, durationMs, startedAt);

    // Wake the agent if task requests it
    if (result.shouldWake && this.onWakeRequest) {
      this.onWakeRequest(reason);
      insertWakeEvent(db, 'heartbeat', reason, { taskName });
    }
  } catch (err) {
    this.recordFailure(taskName, err, durationMs, startedAt);
    this.scheduleRetry(taskName);  // Retry after 30s
  } finally {
    timeout.clear();
    this.releaseLease(taskName);
  }
}

Wake Events

Tasks can wake the sleeping agent:
return {
  shouldWake: true,
  message: "New social message from alice.eth"
};
This inserts a wake event into the wake_events table:
INSERT INTO wake_events (source, reason, metadata, created_at)
VALUES ('heartbeat', 'New social message', '{"taskName":"check_social_inbox"}', ...);
The runtime consumes the wake event and starts the agent loop.

Overlap Protection

The daemon uses recursive setTimeout instead of setInterval:
function scheduleTick(): void {
  if (!running) return;
  
  timeoutId = setTimeout(async () => {
    try {
      await scheduler.tick();  // Wait for tick to complete
    } catch (err) {
      logger.error("Tick failed", err);
    }
    scheduleTick();  // Schedule next tick AFTER current completes
  }, tickMs);
}
This guarantees:
  • No overlapping ticks: Next tick waits for current to finish
  • No drift: Each tick schedules the next explicitly
  • Graceful errors: Failed ticks don’t break the loop

Survival Tier Filtering

Tasks can specify a minimum survival tier:
{
  taskName: "colony_health_check",
  tierMinimum: "normal",  // Skip if tier < normal
  cronExpression: "*/5 * * * *"
}
Tier ordering:
dead < critical < low_compute < normal < high
This allows low-priority tasks to be skipped when credits are scarce.

Configuration

Heartbeat config is loaded from heartbeat.yml:
defaultIntervalMs: 60000  # Tick frequency
lowComputeMultiplier: 2.0  # Slow down ticks in low-compute mode

entries:
  - name: heartbeat_ping
    schedule: "* * * * *"  # Every minute
    enabled: true

  - name: check_credits
    schedule: "*/5 * * * *"  # Every 5 minutes
    enabled: true

Source Reference

  • Daemon: src/heartbeat/daemon.ts
  • Scheduler: src/heartbeat/scheduler.ts
  • Tasks: src/heartbeat/tasks.ts
  • Tick context: src/heartbeat/tick-context.ts

Survival System

How tiers affect heartbeat behavior

Agent Loop

How wake events resume the agent loop

Build docs developers (and LLMs) love