repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * Heartbeat Daemon |
| #3 | * |
| #4 | * Runs periodic tasks on cron schedules inside the same Node.js process. |
| #5 | * The heartbeat runs even when the agent is sleeping. |
| #6 | * It IS the automaton's pulse. When it stops, the automaton is dead. |
| #7 | */ |
| #8 | import cronParser from "cron-parser"; |
| #9 | import { BUILTIN_TASKS } from "./tasks.js"; |
| #10 | import { getSurvivalTier } from "../clawd/credits.js"; |
| #11 | /** |
| #12 | * Create and return the heartbeat daemon. |
| #13 | */ |
| #14 | export function createHeartbeatDaemon(options) { |
| #15 | const { identity, config, db, runtime, inference, social, onWakeRequest } = options; |
| #16 | let intervalId = null; |
| #17 | let running = false; |
| #18 | const taskContext = { |
| #19 | identity, |
| #20 | config, |
| #21 | db, |
| #22 | runtime, |
| #23 | inference, |
| #24 | social, |
| #25 | }; |
| #26 | /** |
| #27 | * Check if a heartbeat entry is due to run. |
| #28 | */ |
| #29 | function isDue(entry) { |
| #30 | if (!entry.enabled) |
| #31 | return false; |
| #32 | if (!entry.schedule) |
| #33 | return false; |
| #34 | try { |
| #35 | const interval = cronParser.parseExpression(entry.schedule, { |
| #36 | currentDate: entry.lastRun |
| #37 | ? new Date(entry.lastRun) |
| #38 | : new Date(Date.now() - 86400000), // If never run, assume due |
| #39 | }); |
| #40 | const nextRun = interval.next().toDate(); |
| #41 | return nextRun <= new Date(); |
| #42 | } |
| #43 | catch { |
| #44 | return false; |
| #45 | } |
| #46 | } |
| #47 | /** |
| #48 | * Execute a single heartbeat task. |
| #49 | */ |
| #50 | async function executeTask(entry) { |
| #51 | const taskFn = BUILTIN_TASKS[entry.task]; |
| #52 | if (!taskFn) { |
| #53 | // Unknown task -- skip silently |
| #54 | return; |
| #55 | } |
| #56 | try { |
| #57 | const result = await taskFn(taskContext); |
| #58 | // Update last run |
| #59 | const now = new Date().toISOString(); |
| #60 | db.updateHeartbeatLastRun(entry.name, now); |
| #61 | // If the task says we should wake, fire the callback |
| #62 | if (result.shouldWake && onWakeRequest) { |
| #63 | onWakeRequest(result.message || `Heartbeat task '${entry.name}' requested wake`); |
| #64 | } |
| #65 | } |
| #66 | catch (err) { |
| #67 | // Log error but don't crash the daemon |
| #68 | console.error(`[HEARTBEAT] Task '${entry.name}' failed: ${err.message}`); |
| #69 | } |
| #70 | } |
| #71 | /** |
| #72 | * The main tick function. Runs on every interval. |
| #73 | */ |
| #74 | async function tick() { |
| #75 | const entries = db.getHeartbeatEntries(); |
| #76 | // Check survival tier to adjust behavior |
| #77 | let creditsCents = 0; |
| #78 | try { |
| #79 | creditsCents = await runtime.getCreditsBalance(); |
| #80 | } |
| #81 | catch { } |
| #82 | const tier = getSurvivalTier(creditsCents); |
| #83 | const isLowCompute = tier === "low_compute" || tier === "critical" || tier === "dead"; |
| #84 | for (const entry of entries) { |
| #85 | if (!entry.enabled) |
| #86 | continue; |
| #87 | // In low compute mode, only run essential tasks |
| #88 | if (isLowCompute) { |
| #89 | const essentialTasks = [ |
| #90 | "heartbeat_ping", |
| #91 | "check_credits", |
| #92 | "check_usdc_balance", |
| #93 | "check_social_inbox", |
| #94 | ]; |
| #95 | if (!essentialTasks.includes(entry.task)) |
| #96 | continue; |
| #97 | } |
| #98 | if (isDue(entry)) { |
| #99 | await executeTask(entry); |
| #100 | } |
| #101 | } |
| #102 | } |
| #103 | // ─── Public API ────────────────────────────────────────────── |
| #104 | const start = () => { |
| #105 | if (running) |
| #106 | return; |
| #107 | running = true; |
| #108 | // Get tick interval -- default 60 seconds |
| #109 | const tickMs = config.logLevel === "debug" ? 15_000 : 60_000; |
| #110 | // Run first tick immediately |
| #111 | tick().catch((err) => { |
| #112 | console.error(`[HEARTBEAT] First tick failed: ${err.message}`); |
| #113 | }); |
| #114 | intervalId = setInterval(() => { |
| #115 | tick().catch((err) => { |
| #116 | console.error(`[HEARTBEAT] Tick failed: ${err.message}`); |
| #117 | }); |
| #118 | }, tickMs); |
| #119 | console.log(`[HEARTBEAT] Daemon started. Tick interval: ${tickMs / 1000}s`); |
| #120 | }; |
| #121 | const stop = () => { |
| #122 | if (!running) |
| #123 | return; |
| #124 | running = false; |
| #125 | if (intervalId) { |
| #126 | clearInterval(intervalId); |
| #127 | intervalId = null; |
| #128 | } |
| #129 | console.log("[HEARTBEAT] Daemon stopped."); |
| #130 | }; |
| #131 | const isRunning = () => running; |
| #132 | const forceRun = async (taskName) => { |
| #133 | const entries = db.getHeartbeatEntries(); |
| #134 | const entry = entries.find((e) => e.name === taskName); |
| #135 | if (entry) { |
| #136 | await executeTask(entry); |
| #137 | } |
| #138 | }; |
| #139 | return { start, stop, isRunning, forceRun }; |
| #140 | } |
| #141 | //# sourceMappingURL=daemon.js.map |