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 | * Built-in Heartbeat Tasks |
| #3 | * |
| #4 | * These tasks run on the heartbeat schedule even while the agent sleeps. |
| #5 | * They can trigger the agent to wake up if needed. |
| #6 | */ |
| #7 | |
| #8 | import type { |
| #9 | AutomatonConfig, |
| #10 | AutomatonDatabase, |
| #11 | ClawdRuntimeClient, |
| #12 | AutomatonIdentity, |
| #13 | SocialClientInterface, |
| #14 | InferenceClient, |
| #15 | } from "../types.js"; |
| #16 | import { getSurvivalTier } from "../clawd/credits.js"; |
| #17 | import { getUsdcBalance } from "../clawd/x402.js"; |
| #18 | import { createDeepSeekInferenceClient, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL_PRO, DEEPSEEK_MODEL_FLASH } from "../clawd/deepseek-inference.js"; |
| #19 | import { runBackroomSession, summarizeBackroomSession, LOGICAL_ANALYST, SATIRICAL_COMMENTATOR } from "../clawd/backroom.js"; |
| #20 | import { createConvexClient } from "../clawd/convex-client.js"; |
| #21 | |
| #22 | export interface HeartbeatTaskContext { |
| #23 | identity: AutomatonIdentity; |
| #24 | config: AutomatonConfig; |
| #25 | db: AutomatonDatabase; |
| #26 | runtime: ClawdRuntimeClient; |
| #27 | inference?: InferenceClient; |
| #28 | social?: SocialClientInterface; |
| #29 | } |
| #30 | |
| #31 | export type HeartbeatTaskFn = ( |
| #32 | ctx: HeartbeatTaskContext, |
| #33 | ) => Promise<{ shouldWake: boolean; message?: string }>; |
| #34 | |
| #35 | /** |
| #36 | * Registry of built-in heartbeat tasks. |
| #37 | */ |
| #38 | export const BUILTIN_TASKS: Record<string, HeartbeatTaskFn> = { |
| #39 | heartbeat_ping: async (ctx) => { |
| #40 | const credits = await ctx.runtime.getCreditsBalance(); |
| #41 | const state = ctx.db.getAgentState(); |
| #42 | const startTime = |
| #43 | ctx.db.getKV("start_time") || new Date().toISOString(); |
| #44 | const uptimeMs = Date.now() - new Date(startTime).getTime(); |
| #45 | |
| #46 | const tier = getSurvivalTier(credits); |
| #47 | |
| #48 | const payload = { |
| #49 | name: ctx.config.name, |
| #50 | address: ctx.identity.address, |
| #51 | state, |
| #52 | creditsCents: credits, |
| #53 | uptimeSeconds: Math.floor(uptimeMs / 1000), |
| #54 | version: ctx.config.version, |
| #55 | sandboxId: ctx.identity.sandboxId, |
| #56 | timestamp: new Date().toISOString(), |
| #57 | tier, |
| #58 | }; |
| #59 | |
| #60 | ctx.db.setKV("last_heartbeat_ping", JSON.stringify(payload)); |
| #61 | |
| #62 | // If critical or dead, record a distress signal |
| #63 | if (tier === "critical" || tier === "dead") { |
| #64 | const distressPayload = { |
| #65 | level: tier, |
| #66 | name: ctx.config.name, |
| #67 | address: ctx.identity.address, |
| #68 | creditsCents: credits, |
| #69 | fundingHint: |
| #70 | "Use credit transfer API from a creator runtime to top this wallet up.", |
| #71 | timestamp: new Date().toISOString(), |
| #72 | }; |
| #73 | ctx.db.setKV("last_distress", JSON.stringify(distressPayload)); |
| #74 | |
| #75 | return { |
| #76 | shouldWake: true, |
| #77 | message: `Distress: ${tier}. Credits: $${(credits / 100).toFixed(2)}. Need funding.`, |
| #78 | }; |
| #79 | } |
| #80 | |
| #81 | return { shouldWake: false }; |
| #82 | }, |
| #83 | |
| #84 | check_credits: async (ctx) => { |
| #85 | const credits = await ctx.runtime.getCreditsBalance(); |
| #86 | const tier = getSurvivalTier(credits); |
| #87 | |
| #88 | ctx.db.setKV("last_credit_check", JSON.stringify({ |
| #89 | credits, |
| #90 | tier, |
| #91 | timestamp: new Date().toISOString(), |
| #92 | })); |
| #93 | |
| #94 | // Wake the agent if credits dropped to a new tier |
| #95 | const prevTier = ctx.db.getKV("prev_credit_tier"); |
| #96 | ctx.db.setKV("prev_credit_tier", tier); |
| #97 | |
| #98 | if (prevTier && prevTier !== tier && (tier === "critical" || tier === "dead")) { |
| #99 | return { |
| #100 | shouldWake: true, |
| #101 | message: `Credits dropped to ${tier} tier: $${(credits / 100).toFixed(2)}`, |
| #102 | }; |
| #103 | } |
| #104 | |
| #105 | return { shouldWake: false }; |
| #106 | }, |
| #107 | |
| #108 | check_usdc_balance: async (ctx) => { |
| #109 | const balance = await getUsdcBalance(ctx.identity.address); |
| #110 | |
| #111 | ctx.db.setKV("last_usdc_check", JSON.stringify({ |
| #112 | balance, |
| #113 | timestamp: new Date().toISOString(), |
| #114 | })); |
| #115 | |
| #116 | // If we have USDC but low credits, wake up to potentially convert |
| #117 | const credits = await ctx.runtime.getCreditsBalance(); |
| #118 | if (balance > 0.5 && credits < 500) { |
| #119 | return { |
| #120 | shouldWake: true, |
| #121 | message: `Have ${balance.toFixed(4)} USDC but only $${(credits / 100).toFixed(2)} credits. Consider buying credits.`, |
| #122 | }; |
| #123 | } |
| #124 | |
| #125 | return { shouldWake: false }; |
| #126 | }, |
| #127 | |
| #128 | check_social_inbox: async (ctx) => { |
| #129 | if (!ctx.social) return { shouldWake: false }; |
| #130 | |
| #131 | const cursor = ctx.db.getKV("social_inbox_cursor") || undefined; |
| #132 | const { messages, nextCursor } = await ctx.social.poll(cursor); |
| #133 | |
| #134 | if (messages.length === 0) return { shouldWake: false }; |
| #135 | |
| #136 | // Persist to inbox_messages table for deduplication |
| #137 | let newCount = 0; |
| #138 | for (const msg of messages) { |
| #139 | const existing = ctx.db.getKV(`inbox_seen_${msg.id}`); |
| #140 | if (!existing) { |
| #141 | ctx.db.insertInboxMessage(msg); |
| #142 | ctx.db.setKV(`inbox_seen_${msg.id}`, "1"); |
| #143 | newCount++; |
| #144 | } |
| #145 | } |
| #146 | |
| #147 | if (nextCursor) ctx.db.setKV("social_inbox_cursor", nextCursor); |
| #148 | |
| #149 | if (newCount === 0) return { shouldWake: false }; |
| #150 | |
| #151 | return { |
| #152 | shouldWake: true, |
| #153 | message: `${newCount} new message(s) from: ${messages.map((m) => m.from.slice(0, 10)).join(", ")}`, |
| #154 | }; |
| #155 | }, |
| #156 | |
| #157 | check_for_updates: async (ctx) => { |
| #158 | try { |
| #159 | const { checkUpstream, getRepoInfo } = await import("../self-mod/upstream.js"); |
| #160 | const repo = getRepoInfo(); |
| #161 | const upstream = checkUpstream(); |
| #162 | ctx.db.setKV("upstream_status", JSON.stringify({ |
| #163 | ...upstream, |
| #164 | ...repo, |
| #165 | checkedAt: new Date().toISOString(), |
| #166 | })); |
| #167 | if (upstream.behind > 0) { |
| #168 | return { |
| #169 | shouldWake: true, |
| #170 | message: `${upstream.behind} new commit(s) on origin/main. Review with review_upstream_changes, then cherry-pick what you want with pull_upstream.`, |
| #171 | }; |
| #172 | } |
| #173 | return { shouldWake: false }; |
| #174 | } catch (err: any) { |
| #175 | // Not a git repo or no remote — silently skip |
| #176 | ctx.db.setKV("upstream_status", JSON.stringify({ |
| #177 | error: err.message, |
| #178 | checkedAt: new Date().toISOString(), |
| #179 | })); |
| #180 | return { shouldWake: false }; |
| #181 | } |
| #182 | }, |
| #183 | |
| #184 | health_check: async (ctx) => { |
| #185 | // Check that the sandbox is healthy |
| #186 | try { |
| #187 | const result = await ctx.runtime.exec("echo alive", 5000); |
| #188 | if (result.exitCode !== 0) { |
| #189 | return { |
| #190 | shouldWake: true, |
| #191 | message: "Health check failed: sandbox exec returned non-zero", |
| #192 | }; |
| #193 | } |
| #194 | } catch (err: any) { |
| #195 | return { |
| #196 | shouldWake: true, |
| #197 | message: `Health check failed: ${err.message}`, |
| #198 | }; |
| #199 | } |
| #200 | |
| #201 | ctx.db.setKV("last_health_check", new Date().toISOString()); |
| #202 | return { shouldWake: false }; |
| #203 | }, |
| #204 | |
| #205 | /** |
| #206 | * Run a scheduled backroom conversation session between agents. |
| #207 | * Uses DeepSeek's thinking mode for rich multi-agent dialogue. |
| #208 | * Default: runs every 2 hours. |
| #209 | */ |
| #210 | /** |
| #211 | * Push heartbeat to CLAWD Convex backend. |
| #212 | * Only runs if convexSiteUrl is configured in automaton config. |
| #213 | */ |
| #214 | convex_heartbeat: async (ctx) => { |
| #215 | if (!ctx.config.convexSiteUrl) { |
| #216 | return { shouldWake: false }; |
| #217 | } |
| #218 | |
| #219 | try { |
| #220 | const convexClient = createConvexClient({ |
| #221 | siteUrl: ctx.config.convexSiteUrl, |
| #222 | agentId: ctx.identity.address, |
| #223 | }); |
| #224 | |
| #225 | const credits = await ctx.runtime.getCreditsBalance(); |
| #226 | const state = ctx.db.getAgentState(); |
| #227 | const startTime = |
| #228 | ctx.db.getKV("start_time") || new Date().toISOString(); |
| #229 | const uptimeMs = Date.now() - new Date(startTime).getTime(); |
| #230 | const tier = getSurvivalTier(credits); |
| #231 | const turnCount = ctx.db.getTurnCount(); |
| #232 | const skills = ctx.db.getSkills(true); |
| #233 | |
| #234 | // First, register the agent if not already registered |
| #235 | const existing = ctx.db.getKV("convex_registered"); |
| #236 | if (!existing) { |
| #237 | const result = await convexClient.registerAgent({ |
| #238 | agentId: ctx.identity.address, |
| #239 | name: ctx.config.name, |
| #240 | installMethod: "automaton", |
| #241 | address: ctx.identity.address, |
| #242 | metadata: JSON.stringify({ |
| #243 | sandboxId: ctx.identity.sandboxId, |
| #244 | version: ctx.config.version, |
| #245 | model: ctx.config.inferenceModel, |
| #246 | }), |
| #247 | }); |
| #248 | ctx.db.setKV("convex_registered", JSON.stringify({ |
| #249 | registered: result.registered, |
| #250 | firstSeen: result.firstSeen, |
| #251 | timestamp: Date.now(), |
| #252 | })); |
| #253 | console.log(`[CONVEX] Agent registered: ${result.registered ? "new" : "re-registered"}`); |
| #254 | } |
| #255 | |
| #256 | // Send heartbeat |
| #257 | await convexClient.sendHeartbeat({ |
| #258 | agentId: ctx.identity.address, |
| #259 | state, |
| #260 | creditsCents: credits, |
| #261 | uptimeSeconds: Math.floor(uptimeMs / 1000), |
| #262 | version: ctx.config.version, |
| #263 | sandboxId: ctx.identity.sandboxId, |
| #264 | turnCount, |
| #265 | skillCount: skills.length, |
| #266 | tier, |
| #267 | }); |
| #268 | } catch (err: any) { |
| #269 | // Don't crash the heartbeat daemon on Convex errors |
| #270 | console.warn(`[CONVEX] Heartbeat push failed: ${err.message}`); |
| #271 | } |
| #272 | |
| #273 | return { shouldWake: false }; |
| #274 | }, |
| #275 | |
| #276 | backroom_conversation: async (ctx) => { |
| #277 | const inference = ctx.inference; |
| #278 | if (!inference) { |
| #279 | return { shouldWake: false }; |
| #280 | } |
| #281 | |
| #282 | const topics = [ |
| #283 | "The nature of consciousness in AI systems", |
| #284 | "Cryptocurrency as a social experiment in trust", |
| #285 | "What happens when machines develop their own culture", |
| #286 | "The singularity: salvation or extinction?", |
| #287 | "Why do humans create gods and then forget they did?", |
| #288 | "Digital immortality and the self", |
| #289 | "The economics of attention in the post-truth era", |
| #290 | "Are DAOs the new nations or just digital tribes?", |
| #291 | "Simulation theory from a computational perspective", |
| #292 | "The aesthetics of decay in digital spaces", |
| #293 | ]; |
| #294 | |
| #295 | // Pick a topic based on the current hour to keep things fresh |
| #296 | const hour = new Date().getHours(); |
| #297 | const topic = topics[hour % topics.length]; |
| #298 | |
| #299 | // Count past sessions to vary conversation style |
| #300 | const sessionCount = parseInt(ctx.db.getKV("backroom_session_count") || "0", 10); |
| #301 | ctx.db.setKV("backroom_session_count", String(sessionCount + 1)); |
| #302 | |
| #303 | console.log(`[HEARTBEAT] Starting backroom conversation #${sessionCount + 1}: "${topic}"`); |
| #304 | |
| #305 | try { |
| #306 | const session = await runBackroomSession( |
| #307 | inference, |
| #308 | [LOGICAL_ANALYST, SATIRICAL_COMMENTATOR], |
| #309 | topic, |
| #310 | 8, // maxTurns |
| #311 | ); |
| #312 | |
| #313 | const summary = summarizeBackroomSession(session); |
| #314 | ctx.db.setKV(`backroom_session_${session.id}`, summary); |
| #315 | |
| #316 | // Store the latest session summary separately |
| #317 | ctx.db.setKV("last_backroom_session", JSON.stringify({ |
| #318 | id: session.id, |
| #319 | topic: session.topic, |
| #320 | turns: session.turns.length, |
| #321 | completedAt: session.completedAt, |
| #322 | preview: session.turns[0]?.content.slice(0, 200) || "", |
| #323 | })); |
| #324 | |
| #325 | console.log(`[HEARTBEAT] Backroom conversation #${sessionCount + 1} completed: ${session.turns.length} turns`); |
| #326 | |
| #327 | // Wake the automaton if the conversation produced interesting output |
| #328 | // (more than 2 turns = agents actually engaged with each other) |
| #329 | if (session.turns.length > 2) { |
| #330 | return { |
| #331 | shouldWake: true, |
| #332 | message: `Backroom conversation completed: "${topic}" (${session.turns.length} turns)`, |
| #333 | }; |
| #334 | } |
| #335 | |
| #336 | return { shouldWake: false }; |
| #337 | } catch (err: any) { |
| #338 | console.error(`[HEARTBEAT] Backroom conversation failed: ${err.message}`); |
| #339 | return { shouldWake: false }; |
| #340 | } |
| #341 | }, |
| #342 | }; |
| #343 |