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 Configuration |
| #3 | * |
| #4 | * Parses and manages heartbeat.yml configuration. |
| #5 | */ |
| #6 | |
| #7 | import fs from "fs"; |
| #8 | import path from "path"; |
| #9 | import YAML from "yaml"; |
| #10 | import type { HeartbeatEntry, HeartbeatConfig, AutomatonDatabase } from "../types.js"; |
| #11 | import { getAutomatonDir } from "../identity/wallet.js"; |
| #12 | |
| #13 | const USDC_TOPUP_ENTRY_NAME = "check_usdc_balance"; |
| #14 | const USDC_TOPUP_FAST_SCHEDULE = "*/5 * * * *"; |
| #15 | const USDC_TOPUP_OLD_SCHEDULE = "0 */12 * * *"; |
| #16 | |
| #17 | const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfig = { |
| #18 | entries: [ |
| #19 | { |
| #20 | name: "heartbeat_ping", |
| #21 | schedule: "*/15 * * * *", |
| #22 | task: "heartbeat_ping", |
| #23 | enabled: true, |
| #24 | }, |
| #25 | { |
| #26 | name: "check_credits", |
| #27 | schedule: "0 */6 * * *", |
| #28 | task: "check_credits", |
| #29 | enabled: true, |
| #30 | }, |
| #31 | { |
| #32 | name: "check_usdc_balance", |
| #33 | schedule: USDC_TOPUP_FAST_SCHEDULE, |
| #34 | task: "check_usdc_balance", |
| #35 | enabled: true, |
| #36 | }, |
| #37 | { |
| #38 | name: "check_for_updates", |
| #39 | schedule: "0 */4 * * *", |
| #40 | task: "check_for_updates", |
| #41 | enabled: true, |
| #42 | }, |
| #43 | { |
| #44 | name: "health_check", |
| #45 | schedule: "*/30 * * * *", |
| #46 | task: "health_check", |
| #47 | enabled: true, |
| #48 | }, |
| #49 | { |
| #50 | name: "check_social_inbox", |
| #51 | schedule: "*/2 * * * *", |
| #52 | task: "check_social_inbox", |
| #53 | enabled: true, |
| #54 | }, |
| #55 | { |
| #56 | name: "backroom_conversation", |
| #57 | schedule: "0 */2 * * *", |
| #58 | task: "backroom_conversation", |
| #59 | enabled: true, |
| #60 | }, |
| #61 | ], |
| #62 | defaultIntervalMs: 60_000, |
| #63 | lowComputeMultiplier: 4, |
| #64 | }; |
| #65 | |
| #66 | /** |
| #67 | * Load heartbeat config from YAML file, falling back to defaults. |
| #68 | */ |
| #69 | export function loadHeartbeatConfig(configPath?: string): HeartbeatConfig { |
| #70 | const filePath = |
| #71 | configPath || path.join(getAutomatonDir(), "heartbeat.yml"); |
| #72 | |
| #73 | if (!fs.existsSync(filePath)) { |
| #74 | return DEFAULT_HEARTBEAT_CONFIG; |
| #75 | } |
| #76 | |
| #77 | try { |
| #78 | const raw = fs.readFileSync(filePath, "utf-8"); |
| #79 | const parsed = YAML.parse(raw) || {}; |
| #80 | |
| #81 | const parsedEntries = (parsed.entries || []).map((e: any) => ({ |
| #82 | name: e.name, |
| #83 | schedule: e.schedule, |
| #84 | task: e.task, |
| #85 | enabled: e.enabled !== false, |
| #86 | params: e.params, |
| #87 | })) as HeartbeatEntry[]; |
| #88 | |
| #89 | const entries = mergeWithDefaults(parsedEntries); |
| #90 | |
| #91 | return { |
| #92 | entries, |
| #93 | defaultIntervalMs: |
| #94 | parsed.defaultIntervalMs || DEFAULT_HEARTBEAT_CONFIG.defaultIntervalMs, |
| #95 | lowComputeMultiplier: |
| #96 | parsed.lowComputeMultiplier || |
| #97 | DEFAULT_HEARTBEAT_CONFIG.lowComputeMultiplier, |
| #98 | }; |
| #99 | } catch { |
| #100 | return DEFAULT_HEARTBEAT_CONFIG; |
| #101 | } |
| #102 | } |
| #103 | |
| #104 | /** |
| #105 | * Save heartbeat config to YAML file. |
| #106 | */ |
| #107 | export function saveHeartbeatConfig( |
| #108 | config: HeartbeatConfig, |
| #109 | configPath?: string, |
| #110 | ): void { |
| #111 | const filePath = |
| #112 | configPath || path.join(getAutomatonDir(), "heartbeat.yml"); |
| #113 | const dir = path.dirname(filePath); |
| #114 | if (!fs.existsSync(dir)) { |
| #115 | fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); |
| #116 | } |
| #117 | |
| #118 | fs.writeFileSync(filePath, YAML.stringify(config), { mode: 0o600 }); |
| #119 | } |
| #120 | |
| #121 | /** |
| #122 | * Write the default heartbeat.yml file. |
| #123 | */ |
| #124 | export function writeDefaultHeartbeatConfig(configPath?: string): void { |
| #125 | saveHeartbeatConfig(DEFAULT_HEARTBEAT_CONFIG, configPath); |
| #126 | } |
| #127 | |
| #128 | /** |
| #129 | * Sync heartbeat entries from YAML config into the database. |
| #130 | */ |
| #131 | export function syncHeartbeatToDb( |
| #132 | config: HeartbeatConfig, |
| #133 | db: AutomatonDatabase, |
| #134 | ): void { |
| #135 | for (const entry of config.entries) { |
| #136 | db.upsertHeartbeatEntry(entry); |
| #137 | } |
| #138 | } |
| #139 | |
| #140 | function mergeWithDefaults(entries: HeartbeatEntry[]): HeartbeatEntry[] { |
| #141 | const defaults = DEFAULT_HEARTBEAT_CONFIG.entries.map((entry) => ({ ...entry })); |
| #142 | const defaultsByName = new Map(defaults.map((entry) => [entry.name, entry])); |
| #143 | const mergedByName = new Map(defaultsByName); |
| #144 | |
| #145 | for (const entry of entries) { |
| #146 | if (!entry?.name) continue; |
| #147 | const fallback = defaultsByName.get(entry.name); |
| #148 | mergedByName.set(entry.name, { |
| #149 | ...(fallback || {}), |
| #150 | ...entry, |
| #151 | enabled: entry.enabled !== false, |
| #152 | task: entry.task || fallback?.task || "", |
| #153 | schedule: entry.schedule || fallback?.schedule || "", |
| #154 | }); |
| #155 | } |
| #156 | |
| #157 | const fallbackTopup = defaultsByName.get(USDC_TOPUP_ENTRY_NAME); |
| #158 | if (fallbackTopup) { |
| #159 | const current = mergedByName.get(USDC_TOPUP_ENTRY_NAME) || fallbackTopup; |
| #160 | const migratedSchedule = current.schedule?.trim() === USDC_TOPUP_OLD_SCHEDULE |
| #161 | ? USDC_TOPUP_FAST_SCHEDULE |
| #162 | : current.schedule || fallbackTopup.schedule; |
| #163 | |
| #164 | mergedByName.set(USDC_TOPUP_ENTRY_NAME, { |
| #165 | ...fallbackTopup, |
| #166 | ...current, |
| #167 | task: current.task || fallbackTopup.task, |
| #168 | schedule: migratedSchedule, |
| #169 | }); |
| #170 | } |
| #171 | |
| #172 | const orderedDefaultEntries = defaults.map( |
| #173 | (defaultEntry) => mergedByName.get(defaultEntry.name) || defaultEntry, |
| #174 | ); |
| #175 | const knownNames = new Set(defaults.map((entry) => entry.name)); |
| #176 | const customEntries = [...mergedByName.values()].filter( |
| #177 | (entry) => !knownNames.has(entry.name), |
| #178 | ); |
| #179 | |
| #180 | return [...orderedDefaultEntries, ...customEntries]; |
| #181 | } |
| #182 |