UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

126 lines (125 loc) 6.8 kB
import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import { s as writeTextAtomic } from "./json-files-2umMHm0W.js"; import "./agent-scope-MrLta7Pq.js"; import { c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js"; import { T as resolveWorkspaceTemplateDir, r as DEFAULT_HEARTBEAT_FILENAME } from "./workspace-B0bmoo98.js"; import { t as note } from "./note-BRSJp0UF.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/commands/doctor-heartbeat-template-repair.ts /** Doctor repair for HEARTBEAT.md files that accidentally contain docs template wrappers. */ const LEGACY_HEARTBEAT_PROSE_TEMPLATE = ["# HEARTBEAT.md", "Keep this file empty unless you want a tiny checklist. Keep it small."]; const LEGACY_HEARTBEAT_HEADING_FENCED_TEMPLATE = [ "# HEARTBEAT.md Template", "```markdown", "# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically.", "```" ]; const LEGACY_HEARTBEAT_FENCED_TEMPLATE = [ "```markdown", "# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically.", "```" ]; const LEGACY_HEARTBEAT_FENCED_RELATED_TEMPLATE = [ "```markdown", "# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically.", "```", "## Related", "- [Heartbeat config](/gateway/config-agents)" ]; const DOCS_HEARTBEAT_TEMPLATE_PAGE_AS_TEMPLATE = [ "# HEARTBEAT.md template", "`HEARTBEAT.md` lives in the agent workspace. Keep the file empty, or with only Markdown comments and headings, when you want OpenClaw to skip heartbeat model calls.", "The default runtime template is:", "```markdown", "# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically.", "```", "Add short tasks below the comments only when you want the agent to check something periodically. Keep heartbeat instructions small because they are read during recurring wakes.", "## Related", "- [Heartbeat config](/gateway/config-agents)" ]; const HEARTBEAT_DEFAULT_BODY_LINES = ["# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically."]; const DIRTY_HEARTBEAT_DOC_WRAPPER_LINES = new Set([ "```markdown", "# HEARTBEAT.md Template", "# HEARTBEAT.md template", "- [Heartbeat config](/gateway/config-agents)" ]); const KNOWN_DIRTY_HEARTBEAT_TEMPLATE_LINES = new Set([ "```markdown", "```", "# HEARTBEAT.md Template", "# HEARTBEAT.md template", "`HEARTBEAT.md` lives in the agent workspace. Keep the file empty, or with only Markdown comments and headings, when you want OpenClaw to skip heartbeat model calls.", "The default runtime template is:", "Add short tasks below the comments only when you want the agent to check something periodically. Keep heartbeat instructions small because they are read during recurring wakes.", ...LEGACY_HEARTBEAT_PROSE_TEMPLATE, "# Keep this file empty (or with only comments) to skip heartbeat API calls.", "# Add tasks below when you want the agent to check something periodically.", "## Related", "- [Heartbeat config](/gateway/config-agents)" ]); const KNOWN_REPAIRABLE_DIRTY_HEARTBEAT_TEMPLATES = [ LEGACY_HEARTBEAT_PROSE_TEMPLATE, LEGACY_HEARTBEAT_HEADING_FENCED_TEMPLATE, LEGACY_HEARTBEAT_FENCED_TEMPLATE, LEGACY_HEARTBEAT_FENCED_RELATED_TEMPLATE, DOCS_HEARTBEAT_TEMPLATE_PAGE_AS_TEMPLATE ]; function linesEqual(left, right) { return left.length === right.length && left.every((line, index) => line === right[index]); } /** Classifies heartbeat template content as clean, repairable, or risky because it has user text. */ function analyzeHeartbeatTemplateForRepair(content) { const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0); if (KNOWN_REPAIRABLE_DIRTY_HEARTBEAT_TEMPLATES.some((template) => linesEqual(lines, template))) return { status: "dirty-template" }; const hasDefaultTemplateBody = HEARTBEAT_DEFAULT_BODY_LINES.every((line) => lines.includes(line)); const hasDirtyDocWrapper = lines.some((line) => DIRTY_HEARTBEAT_DOC_WRAPPER_LINES.has(line)); const hasLegacyProseTemplate = LEGACY_HEARTBEAT_PROSE_TEMPLATE.every((line) => lines.includes(line)); if ((!hasDefaultTemplateBody || !hasDirtyDocWrapper) && !hasLegacyProseTemplate) return { status: "clean" }; return { status: "dirty-template-with-custom-content", customLines: lines.filter((line) => !KNOWN_DIRTY_HEARTBEAT_TEMPLATE_LINES.has(line)) }; } async function readCleanHeartbeatTemplate() { const templateDir = await resolveWorkspaceTemplateDir(); const templatePath = path.join(templateDir, DEFAULT_HEARTBEAT_FILENAME); return await fs.readFile(templatePath, "utf-8"); } /** Replaces known dirty heartbeat templates with the clean runtime template when repair is enabled. */ async function maybeRepairHeartbeatTemplate(params) { const workspaceDir = resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); const heartbeatPath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME); let content; try { content = await fs.readFile(heartbeatPath, "utf-8"); } catch (error) { if (error?.code === "ENOENT") return; note(`Could not inspect ${shortenHomePath(heartbeatPath)}: ${formatErrorMessage(error)}`, "Heartbeat template"); return; } const analysis = analyzeHeartbeatTemplateForRepair(content); if (analysis.status === "clean") return; if (analysis.status === "dirty-template-with-custom-content") { note([`${shortenHomePath(heartbeatPath)} contains an older heartbeat template wrapper plus custom or unrecognized content.`, "Doctor left it unchanged so it does not delete user tasks. Remove the fenced template and Related lines manually if they are not intentional."].join("\n"), "Heartbeat template"); return; } if (!params.shouldRepair) { note([`${shortenHomePath(heartbeatPath)} contains an older heartbeat documentation template.`, "Run \"openclaw doctor --fix\" to replace it with the clean heartbeat template."].join("\n"), "Heartbeat template"); return; } try { await writeTextAtomic(heartbeatPath, await readCleanHeartbeatTemplate(), { mode: 384 }); note(`Replaced ${shortenHomePath(heartbeatPath)} with the clean heartbeat template.`, "Doctor changes"); } catch (error) { note(`Could not repair ${shortenHomePath(heartbeatPath)}: ${formatErrorMessage(error)}`, "Heartbeat template"); } } //#endregion export { maybeRepairHeartbeatTemplate };