diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c9080de..4788e8446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. ### Fixes +- Heartbeat: default interval now 30m with a new default prompt + HEARTBEAT.md template. - Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327. - Docs: add missing `ui:install` setup step in the README. Thanks @hugobarauna for PR #300. - Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312. diff --git a/docs/clawd.md b/docs/clawd.md index 871aba585..1c62d396b 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -18,7 +18,7 @@ You’re putting an agent in a position to: Start conservative: - Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac). - Use a dedicated WhatsApp number for the assistant. -- Keep heartbeats disabled until you trust the setup (omit `agent.heartbeat` or set `agent.heartbeat.every: "0m"`). +- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agent.heartbeat.every: "0m"`. ## Prerequisites @@ -161,7 +161,9 @@ Example: ## Heartbeats (proactive mode) -When `agent.heartbeat.every` is set to a positive interval, CLAWDBOT periodically runs a heartbeat prompt (default: `HEARTBEAT`). +By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt: +`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` +Set `agent.heartbeat.every: "0m"` to disable. - If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat. diff --git a/docs/configuration.md b/docs/configuration.md index 680dac9e7..869945fa0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -786,14 +786,17 @@ Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require `ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment. `agent.heartbeat` configures periodic heartbeat runs: -- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Omit or set - `0m` to disable. +- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default: + `30m`. Set `0m` to disable. - `model`: optional override model for heartbeat runs (`provider/model`). -- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `imessage`, `none`). Default: `last`. -- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram). -- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`). +- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`. +- `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram). +- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). +Heartbeats run full agent turns. Shorter intervals burn more tokens; adjust `every` +and/or `model` accordingly. + `agent.bash` configures background bash defaults: - `backgroundMs`: time before auto-background (ms, default 10000) - `timeoutSec`: auto-kill after this runtime (seconds, default 1800) diff --git a/docs/cron.md b/docs/cron.md index cfc8ab4c4..5c76c8b8d 100644 --- a/docs/cron.md +++ b/docs/cron.md @@ -14,7 +14,7 @@ Last updated: 2025-12-13 ## Context Clawdbot already has: -- A **gateway heartbeat runner** that runs the agent with `HEARTBEAT` and suppresses `HEARTBEAT_OK` ([`src/infra/heartbeat-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-runner.ts)). +- A **gateway heartbeat runner** that runs the agent with the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) and suppresses `HEARTBEAT_OK` ([`src/infra/heartbeat-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-runner.ts)). - A lightweight, in-memory **system event queue** (`enqueueSystemEvent`) that is injected into the next **main session** turn (`drainSystemEvents` in [`src/auto-reply/reply.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/auto-reply/reply.ts)). - A WebSocket **Gateway** daemon that is intended to be always-on ([`docs/gateway.md`](https://docs.clawd.bot/gateway)). diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 0f57d13f4..6fb021a4d 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -9,13 +9,16 @@ Heartbeat runs periodic agent turns in the **main session** so the model can surface anything that needs attention without spamming the user. ## Prompt contract -- Heartbeat body defaults to `HEARTBEAT` (configurable via `agent.heartbeat.prompt`). +- Heartbeat body defaults to: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` (configurable via `agent.heartbeat.prompt`). - If nothing needs attention, the model should reply `HEARTBEAT_OK`. - During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at the **start or end** of the reply. Clawdbot strips the token and discards the reply if the remaining content is **≤ `ackMaxChars`** (default: 30). - If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially. - For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text. +- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does + not append extra body text. The system prompt includes a Heartbeats section + and the run is flagged as a heartbeat internally. ### Stray `HEARTBEAT_OK` outside heartbeats If the model accidentally includes `HEARTBEAT_OK` at the start or end of a @@ -35,11 +38,11 @@ and final replies: { agent: { heartbeat: { - every: "30m", // duration string: ms|s|m|h (0m disables) + every: "30m", // default: 30m (0m disables) model: "anthropic/claude-opus-4-5", - target: "last", // last | whatsapp | telegram | none - to: "+15551234567", // optional override for whatsapp/telegram - prompt: "HEARTBEAT", // optional override + target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none + to: "+15551234567", // optional provider-specific override (e.g. E.164 or chat id) + prompt: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.", ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK } } @@ -47,17 +50,28 @@ and final replies: ``` ### Fields -- `every`: heartbeat interval (duration string; default unit minutes). Omit or set - to `0m` to disable. +- `every`: heartbeat interval (duration string; default unit minutes). Default: + `30m`. Set to `0m` to disable. - `model`: optional model override for heartbeat runs (`provider/model`). - `target`: where heartbeat output is delivered. - `last` (default): send to the last used external provider. - - `whatsapp` / `telegram`: force the provider (optionally set `to`). + - `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage`: force the provider (optionally set `to`). - `none`: do not deliver externally; output stays in the session (WebChat-visible). - `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram). -- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`). +- `prompt`: optional override for the heartbeat body (default shown above). Safe to + change; heartbeat acks are still keyed off `HEARTBEAT_OK`. - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). +## Cost awareness +Heartbeats run full agent turns. Shorter intervals burn more tokens. If you +don’t need frequent checks, increase `every`, pick a cheaper `model`, or set +`target: "none"` to keep results internal. + +## HEARTBEAT.md (optional) +If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the +agent to read it. Keep it tiny (short checklist or reminders) to avoid prompt +bloat. + ## Behavior - Runs in the main session (`main`, or `global` when scope is global). - Uses the main lane queue; if requests are in flight, the wake is retried. @@ -66,6 +80,12 @@ and final replies: - If `target` resolves to no external destination (no last route or `none`), the heartbeat still runs but no outbound message is sent. +## Ideas for use +- Check up on the user (light, respectful pings during daytime). +- Handle mundane tasks (triage inboxes, summarize queues, refresh notes). +- Nudge on open loops or reminders. +- Background monitoring (health checks, status polling, low-priority alerts). + ## Wake hook - The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an immediate run (`requestHeartbeatNow`). diff --git a/docs/templates/AGENTS.md b/docs/templates/AGENTS.md index f8e3ed40c..051f19c00 100644 --- a/docs/templates/AGENTS.md +++ b/docs/templates/AGENTS.md @@ -115,7 +115,12 @@ Skills provide your tools. When you need one, check its `SKILL.md`. Keep local n ## 💓 Heartbeats - Be Proactive! -When you receive a `HEARTBEAT` message, don't just reply `HEARTBEAT_OK` every time. Use heartbeats productively! +When you receive a heartbeat poll (message matches the configured heartbeat prompt), don't just reply `HEARTBEAT_OK` every time. Use heartbeats productively! + +Default heartbeat prompt: +`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` + +You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small. **Things to check (rotate through these, 2-4 times per day):** - **Emails** - Any urgent unread messages? diff --git a/docs/templates/HEARTBEAT.md b/docs/templates/HEARTBEAT.md new file mode 100644 index 000000000..4d300f421 --- /dev/null +++ b/docs/templates/HEARTBEAT.md @@ -0,0 +1,8 @@ +--- +summary: "Workspace template for HEARTBEAT.md" +read_when: + - Bootstrapping a workspace manually +--- +# HEARTBEAT.md + +Keep this file empty unless you want a tiny checklist for heartbeat runs. Keep it small. diff --git a/docs/thinking.md b/docs/thinking.md index 81b903d51..86f697581 100644 --- a/docs/thinking.md +++ b/docs/thinking.md @@ -38,7 +38,7 @@ read_when: - Elevated mode docs live in [`docs/elevated.md`](https://docs.clawd.bot/elevated). ## Heartbeats -- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats). +- Heartbeat probe body is the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats). ## Web chat UI - The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads. diff --git a/docs/whatsapp.md b/docs/whatsapp.md index 71321f6e8..e23c597bc 100644 --- a/docs/whatsapp.md +++ b/docs/whatsapp.md @@ -115,7 +115,7 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number ## Heartbeats - **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s). - **Agent heartbeat** is global (`agent.heartbeat.*`) and runs in the main session. - - Uses `HEARTBEAT` prompt + `HEARTBEAT_OK` skip behavior. + - Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) + `HEARTBEAT_OK` skip behavior. - Delivery defaults to the last used provider (or configured target). ## Reconnect behavior diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 78e895c24..a80b6a982 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -12,6 +12,7 @@ import { SettingsManager, type Skill, } from "@mariozechner/pi-coding-agent"; +import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js"; import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js"; import { formatToolAggregate } from "../auto-reply/tool-meta.js"; import type { ClawdbotConfig } from "../config/config.js"; @@ -469,6 +470,9 @@ export async function compactEmbeddedPiSession(params: { extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, reasoningTagHint, + heartbeatPrompt: resolveHeartbeatPrompt( + params.config?.agent?.heartbeat?.prompt, + ), runtimeInfo, sandboxInfo, toolNames: tools.map((tool) => tool.name), @@ -765,6 +769,9 @@ export async function runEmbeddedPiAgent(params: { extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, reasoningTagHint, + heartbeatPrompt: resolveHeartbeatPrompt( + params.config?.agent?.heartbeat?.prompt, + ), runtimeInfo, sandboxInfo, toolNames: tools.map((tool) => tool.name), diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 4528d372d..d5c40f51b 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -9,6 +9,7 @@ export function buildAgentSystemPromptAppend(params: { toolNames?: string[]; userTimezone?: string; userTime?: string; + heartbeatPrompt?: string; runtimeInfo?: { host?: string; os?: string; @@ -113,6 +114,10 @@ export function buildAgentSystemPromptAppend(params: { : undefined; const userTimezone = params.userTimezone?.trim(); const userTime = params.userTime?.trim(); + const heartbeatPrompt = params.heartbeatPrompt?.trim(); + const heartbeatPromptLine = heartbeatPrompt + ? `Heartbeat prompt: ${heartbeatPrompt}` + : "Heartbeat prompt: (configured)"; const runtimeInfo = params.runtimeInfo; const runtimeLines: string[] = []; if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`); @@ -207,7 +212,8 @@ export function buildAgentSystemPromptAppend(params: { lines.push( "## Heartbeats", - 'If you receive a heartbeat poll (a user message containing just "HEARTBEAT"), and there is nothing that needs attention, reply exactly:', + heartbeatPromptLine, + "If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:", "HEARTBEAT_OK", 'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).', 'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.', diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index 0f2afafa0..89e8c5a25 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -23,9 +23,11 @@ describe("ensureAgentWorkspace", () => { const identity = path.join(path.resolve(nested), "IDENTITY.md"); const user = path.join(path.resolve(nested), "USER.md"); + const heartbeat = path.join(path.resolve(nested), "HEARTBEAT.md"); const bootstrap = path.join(path.resolve(nested), "BOOTSTRAP.md"); await expect(fs.stat(identity)).resolves.toBeDefined(); await expect(fs.stat(user)).resolves.toBeDefined(); + await expect(fs.stat(heartbeat)).resolves.toBeDefined(); await expect(fs.stat(bootstrap)).resolves.toBeDefined(); }); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 8351f870b..a27dc7e5f 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -22,6 +22,7 @@ export const DEFAULT_SOUL_FILENAME = "SOUL.md"; export const DEFAULT_TOOLS_FILENAME = "TOOLS.md"; export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md"; export const DEFAULT_USER_FILENAME = "USER.md"; +export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md"; export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md"; const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdbot Workspace @@ -53,6 +54,9 @@ git commit -m "Add agent workspace" - On session start, read today + yesterday if present. - Capture durable facts, preferences, and decisions; avoid secrets. +## Heartbeats (optional) +- HEARTBEAT.md can hold a tiny checklist for heartbeat runs; keep it small. + ## Customize - Add your preferred style, rules, and "memory" here. `; @@ -83,6 +87,12 @@ It does not define which tools exist; Clawdbot provides built-in tools internall Add whatever else you want the assistant to know about your local toolchain. `; +const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md - Optional heartbeat notes + +Keep this file small. Leave it empty unless you want a short checklist or reminders +to follow during heartbeat runs. +`; + const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after) Hello. I was just born. @@ -174,6 +184,7 @@ export type WorkspaceBootstrapFileName = | typeof DEFAULT_TOOLS_FILENAME | typeof DEFAULT_IDENTITY_FILENAME | typeof DEFAULT_USER_FILENAME + | typeof DEFAULT_HEARTBEAT_FILENAME | typeof DEFAULT_BOOTSTRAP_FILENAME; export type WorkspaceBootstrapFile = { @@ -205,6 +216,7 @@ export async function ensureAgentWorkspace(params?: { toolsPath?: string; identityPath?: string; userPath?: string; + heartbeatPath?: string; bootstrapPath?: string; }> { const rawDir = params?.dir?.trim() @@ -220,10 +232,18 @@ export async function ensureAgentWorkspace(params?: { const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME); const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME); const userPath = path.join(dir, DEFAULT_USER_FILENAME); + const heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME); const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME); const isBrandNewWorkspace = await (async () => { - const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath]; + const paths = [ + agentsPath, + soulPath, + toolsPath, + identityPath, + userPath, + heartbeatPath, + ]; const existing = await Promise.all( paths.map(async (p) => { try { @@ -257,6 +277,10 @@ export async function ensureAgentWorkspace(params?: { DEFAULT_USER_FILENAME, DEFAULT_USER_TEMPLATE, ); + const heartbeatTemplate = await loadTemplate( + DEFAULT_HEARTBEAT_FILENAME, + DEFAULT_HEARTBEAT_TEMPLATE, + ); const bootstrapTemplate = await loadTemplate( DEFAULT_BOOTSTRAP_FILENAME, DEFAULT_BOOTSTRAP_TEMPLATE, @@ -267,6 +291,7 @@ export async function ensureAgentWorkspace(params?: { await writeFileIfMissing(toolsPath, toolsTemplate); await writeFileIfMissing(identityPath, identityTemplate); await writeFileIfMissing(userPath, userTemplate); + await writeFileIfMissing(heartbeatPath, heartbeatTemplate); if (isBrandNewWorkspace) { await writeFileIfMissing(bootstrapPath, bootstrapTemplate); } @@ -278,6 +303,7 @@ export async function ensureAgentWorkspace(params?: { toolsPath, identityPath, userPath, + heartbeatPath, bootstrapPath, }; } @@ -311,6 +337,10 @@ export async function loadWorkspaceBootstrapFiles( name: DEFAULT_USER_FILENAME, filePath: path.join(resolvedDir, DEFAULT_USER_FILENAME), }, + { + name: DEFAULT_HEARTBEAT_FILENAME, + filePath: path.join(resolvedDir, DEFAULT_HEARTBEAT_FILENAME), + }, { name: DEFAULT_BOOTSTRAP_FILENAME, filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME), diff --git a/src/auto-reply/heartbeat.ts b/src/auto-reply/heartbeat.ts index d4b57bfe2..3f7856ed7 100644 --- a/src/auto-reply/heartbeat.ts +++ b/src/auto-reply/heartbeat.ts @@ -1,8 +1,15 @@ import { HEARTBEAT_TOKEN } from "./tokens.js"; -export const HEARTBEAT_PROMPT = "HEARTBEAT"; +export const HEARTBEAT_PROMPT = + "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."; +export const DEFAULT_HEARTBEAT_EVERY = "30m"; export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 30; +export function resolveHeartbeatPrompt(raw?: string): string { + const trimmed = typeof raw === "string" ? raw.trim() : ""; + return trimmed || HEARTBEAT_PROMPT; +} + export type StripHeartbeatMode = "heartbeat" | "message"; function stripTokenAtEdges(raw: string): { text: string; didStrip: boolean } { diff --git a/src/config/types.ts b/src/config/types.ts index 0f76c3beb..a103ab1a7 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -853,7 +853,7 @@ export type ClawdbotConfig = { typingIntervalSeconds?: number; /** Periodic background heartbeat runs. */ heartbeat?: { - /** Heartbeat interval (duration string, default unit: minutes). */ + /** Heartbeat interval (duration string, default unit: minutes; default: 30m). */ every?: string; /** Heartbeat model override (provider/model). */ model?: string; @@ -869,7 +869,7 @@ export type ClawdbotConfig = { | "none"; /** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */ to?: string; - /** Override the heartbeat prompt body (default: "HEARTBEAT"). */ + /** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."). */ prompt?: string; /** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */ ackMaxChars?: number; diff --git a/src/infra/heartbeat-runner.test.ts b/src/infra/heartbeat-runner.test.ts index 39a05c1fd..1a74d49aa 100644 --- a/src/infra/heartbeat-runner.test.ts +++ b/src/infra/heartbeat-runner.test.ts @@ -13,8 +13,11 @@ import { } from "./heartbeat-runner.js"; describe("resolveHeartbeatIntervalMs", () => { - it("returns null when unset or invalid", () => { - expect(resolveHeartbeatIntervalMs({})).toBeNull(); + it("returns default when unset", () => { + expect(resolveHeartbeatIntervalMs({})).toBe(30 * 60_000); + }); + + it("returns null when invalid or zero", () => { expect( resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "0m" } } }), ).toBeNull(); diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 316334f0a..1fa7065bc 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -1,7 +1,8 @@ import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js"; import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, - HEARTBEAT_PROMPT, + DEFAULT_HEARTBEAT_EVERY, + resolveHeartbeatPrompt as resolveHeartbeatPromptText, stripHeartbeatToken, } from "../auto-reply/heartbeat.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; @@ -83,7 +84,8 @@ export function resolveHeartbeatIntervalMs( cfg: ClawdbotConfig, overrideEvery?: string, ) { - const raw = overrideEvery ?? cfg.agent?.heartbeat?.every; + const raw = + overrideEvery ?? cfg.agent?.heartbeat?.every ?? DEFAULT_HEARTBEAT_EVERY; if (!raw) return null; const trimmed = String(raw).trim(); if (!trimmed) return null; @@ -98,9 +100,7 @@ export function resolveHeartbeatIntervalMs( } export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) { - const raw = cfg.agent?.heartbeat?.prompt; - const trimmed = typeof raw === "string" ? raw.trim() : ""; - return trimmed || HEARTBEAT_PROMPT; + return resolveHeartbeatPromptText(cfg.agent?.heartbeat?.prompt); } function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) { diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 1618fd965..2532923df 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -10,6 +10,7 @@ import { import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, HEARTBEAT_PROMPT, + resolveHeartbeatPrompt, stripHeartbeatToken, } from "../auto-reply/heartbeat.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; @@ -339,7 +340,7 @@ export async function runWebHeartbeatOnce(opts: { const replyResult = await replyResolver( { - Body: HEARTBEAT_PROMPT, + Body: resolveHeartbeatPrompt(cfg.agent?.heartbeat?.prompt), From: to, To: to, MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,