feat: update heartbeat defaults
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
||||||
|
|
||||||
### Fixes
|
### 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.
|
- 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.
|
- 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.
|
- Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ You’re putting an agent in a position to:
|
|||||||
Start conservative:
|
Start conservative:
|
||||||
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
|
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
|
||||||
- Use a dedicated WhatsApp number for the assistant.
|
- 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
|
## Prerequisites
|
||||||
|
|
||||||
@@ -161,7 +161,9 @@ Example:
|
|||||||
|
|
||||||
## Heartbeats (proactive mode)
|
## 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.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
|
||||||
|
|
||||||
|
|||||||
@@ -786,14 +786,17 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
|||||||
`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
|
`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
|
||||||
|
|
||||||
`agent.heartbeat` configures periodic heartbeat runs:
|
`agent.heartbeat` configures periodic heartbeat runs:
|
||||||
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Omit or set
|
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
|
||||||
`0m` to disable.
|
`30m`. Set `0m` to disable.
|
||||||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
- `model`: optional override model for heartbeat runs (`provider/model`).
|
||||||
- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `imessage`, `none`). Default: `last`.
|
- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
|
||||||
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
- `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: `HEARTBEAT`).
|
- `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).
|
- `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:
|
`agent.bash` configures background bash defaults:
|
||||||
- `backgroundMs`: time before auto-background (ms, default 10000)
|
- `backgroundMs`: time before auto-background (ms, default 10000)
|
||||||
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Last updated: 2025-12-13
|
|||||||
## Context
|
## Context
|
||||||
|
|
||||||
Clawdbot already has:
|
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 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)).
|
- A WebSocket **Gateway** daemon that is intended to be always-on ([`docs/gateway.md`](https://docs.clawd.bot/gateway)).
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
surface anything that needs attention without spamming the user.
|
||||||
|
|
||||||
## Prompt contract
|
## 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`.
|
- If nothing needs attention, the model should reply `HEARTBEAT_OK`.
|
||||||
- During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at
|
- 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
|
the **start or end** of the reply. Clawdbot strips the token and discards the
|
||||||
reply if the remaining content is **≤ `ackMaxChars`** (default: 30).
|
reply if the remaining content is **≤ `ackMaxChars`** (default: 30).
|
||||||
- If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially.
|
- 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.
|
- 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
|
### Stray `HEARTBEAT_OK` outside heartbeats
|
||||||
If the model accidentally includes `HEARTBEAT_OK` at the start or end of a
|
If the model accidentally includes `HEARTBEAT_OK` at the start or end of a
|
||||||
@@ -35,11 +38,11 @@ and final replies:
|
|||||||
{
|
{
|
||||||
agent: {
|
agent: {
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
every: "30m", // duration string: ms|s|m|h (0m disables)
|
every: "30m", // default: 30m (0m disables)
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-5",
|
||||||
target: "last", // last | whatsapp | telegram | none
|
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
||||||
to: "+15551234567", // optional override for whatsapp/telegram
|
to: "+15551234567", // optional provider-specific override (e.g. E.164 or chat id)
|
||||||
prompt: "HEARTBEAT", // optional override
|
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
|
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,17 +50,28 @@ and final replies:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
- `every`: heartbeat interval (duration string; default unit minutes). Omit or set
|
- `every`: heartbeat interval (duration string; default unit minutes). Default:
|
||||||
to `0m` to disable.
|
`30m`. Set to `0m` to disable.
|
||||||
- `model`: optional model override for heartbeat runs (`provider/model`).
|
- `model`: optional model override for heartbeat runs (`provider/model`).
|
||||||
- `target`: where heartbeat output is delivered.
|
- `target`: where heartbeat output is delivered.
|
||||||
- `last` (default): send to the last used external provider.
|
- `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).
|
- `none`: do not deliver externally; output stays in the session (WebChat-visible).
|
||||||
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
- `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).
|
- `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
|
## Behavior
|
||||||
- Runs in the main session (`main`, or `global` when scope is global).
|
- 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.
|
- 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
|
- If `target` resolves to no external destination (no last route or `none`), the
|
||||||
heartbeat still runs but no outbound message is sent.
|
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
|
## Wake hook
|
||||||
- The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an
|
- The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an
|
||||||
immediate run (`requestHeartbeatNow`).
|
immediate run (`requestHeartbeatNow`).
|
||||||
|
|||||||
7
docs/templates/AGENTS.md
vendored
7
docs/templates/AGENTS.md
vendored
@@ -115,7 +115,12 @@ Skills provide your tools. When you need one, check its `SKILL.md`. Keep local n
|
|||||||
|
|
||||||
## 💓 Heartbeats - Be Proactive!
|
## 💓 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):**
|
**Things to check (rotate through these, 2-4 times per day):**
|
||||||
- **Emails** - Any urgent unread messages?
|
- **Emails** - Any urgent unread messages?
|
||||||
|
|||||||
8
docs/templates/HEARTBEAT.md
vendored
Normal file
8
docs/templates/HEARTBEAT.md
vendored
Normal file
@@ -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.
|
||||||
@@ -38,7 +38,7 @@ read_when:
|
|||||||
- Elevated mode docs live in [`docs/elevated.md`](https://docs.clawd.bot/elevated).
|
- Elevated mode docs live in [`docs/elevated.md`](https://docs.clawd.bot/elevated).
|
||||||
|
|
||||||
## Heartbeats
|
## 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
|
## Web chat UI
|
||||||
- The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads.
|
- The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads.
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number
|
|||||||
## Heartbeats
|
## Heartbeats
|
||||||
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
|
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
|
||||||
- **Agent heartbeat** is global (`agent.heartbeat.*`) and runs in the main session.
|
- **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).
|
- Delivery defaults to the last used provider (or configured target).
|
||||||
|
|
||||||
## Reconnect behavior
|
## Reconnect behavior
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
SettingsManager,
|
SettingsManager,
|
||||||
type Skill,
|
type Skill,
|
||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||||
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
@@ -469,6 +470,9 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
reasoningTagHint,
|
reasoningTagHint,
|
||||||
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
|
params.config?.agent?.heartbeat?.prompt,
|
||||||
|
),
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
sandboxInfo,
|
sandboxInfo,
|
||||||
toolNames: tools.map((tool) => tool.name),
|
toolNames: tools.map((tool) => tool.name),
|
||||||
@@ -765,6 +769,9 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
reasoningTagHint,
|
reasoningTagHint,
|
||||||
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
|
params.config?.agent?.heartbeat?.prompt,
|
||||||
|
),
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
sandboxInfo,
|
sandboxInfo,
|
||||||
toolNames: tools.map((tool) => tool.name),
|
toolNames: tools.map((tool) => tool.name),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
toolNames?: string[];
|
toolNames?: string[];
|
||||||
userTimezone?: string;
|
userTimezone?: string;
|
||||||
userTime?: string;
|
userTime?: string;
|
||||||
|
heartbeatPrompt?: string;
|
||||||
runtimeInfo?: {
|
runtimeInfo?: {
|
||||||
host?: string;
|
host?: string;
|
||||||
os?: string;
|
os?: string;
|
||||||
@@ -113,6 +114,10 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const userTimezone = params.userTimezone?.trim();
|
const userTimezone = params.userTimezone?.trim();
|
||||||
const userTime = params.userTime?.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 runtimeInfo = params.runtimeInfo;
|
||||||
const runtimeLines: string[] = [];
|
const runtimeLines: string[] = [];
|
||||||
if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`);
|
if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`);
|
||||||
@@ -207,7 +212,8 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
"## Heartbeats",
|
"## 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",
|
"HEARTBEAT_OK",
|
||||||
'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).',
|
'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.',
|
'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.',
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ describe("ensureAgentWorkspace", () => {
|
|||||||
|
|
||||||
const identity = path.join(path.resolve(nested), "IDENTITY.md");
|
const identity = path.join(path.resolve(nested), "IDENTITY.md");
|
||||||
const user = path.join(path.resolve(nested), "USER.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");
|
const bootstrap = path.join(path.resolve(nested), "BOOTSTRAP.md");
|
||||||
await expect(fs.stat(identity)).resolves.toBeDefined();
|
await expect(fs.stat(identity)).resolves.toBeDefined();
|
||||||
await expect(fs.stat(user)).resolves.toBeDefined();
|
await expect(fs.stat(user)).resolves.toBeDefined();
|
||||||
|
await expect(fs.stat(heartbeat)).resolves.toBeDefined();
|
||||||
await expect(fs.stat(bootstrap)).resolves.toBeDefined();
|
await expect(fs.stat(bootstrap)).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const DEFAULT_SOUL_FILENAME = "SOUL.md";
|
|||||||
export const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
export const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
||||||
export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
||||||
export const DEFAULT_USER_FILENAME = "USER.md";
|
export const DEFAULT_USER_FILENAME = "USER.md";
|
||||||
|
export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
||||||
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
||||||
|
|
||||||
const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdbot Workspace
|
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.
|
- On session start, read today + yesterday if present.
|
||||||
- Capture durable facts, preferences, and decisions; avoid secrets.
|
- Capture durable facts, preferences, and decisions; avoid secrets.
|
||||||
|
|
||||||
|
## Heartbeats (optional)
|
||||||
|
- HEARTBEAT.md can hold a tiny checklist for heartbeat runs; keep it small.
|
||||||
|
|
||||||
## Customize
|
## Customize
|
||||||
- Add your preferred style, rules, and "memory" here.
|
- 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.
|
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)
|
const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after)
|
||||||
|
|
||||||
Hello. I was just born.
|
Hello. I was just born.
|
||||||
@@ -174,6 +184,7 @@ export type WorkspaceBootstrapFileName =
|
|||||||
| typeof DEFAULT_TOOLS_FILENAME
|
| typeof DEFAULT_TOOLS_FILENAME
|
||||||
| typeof DEFAULT_IDENTITY_FILENAME
|
| typeof DEFAULT_IDENTITY_FILENAME
|
||||||
| typeof DEFAULT_USER_FILENAME
|
| typeof DEFAULT_USER_FILENAME
|
||||||
|
| typeof DEFAULT_HEARTBEAT_FILENAME
|
||||||
| typeof DEFAULT_BOOTSTRAP_FILENAME;
|
| typeof DEFAULT_BOOTSTRAP_FILENAME;
|
||||||
|
|
||||||
export type WorkspaceBootstrapFile = {
|
export type WorkspaceBootstrapFile = {
|
||||||
@@ -205,6 +216,7 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
toolsPath?: string;
|
toolsPath?: string;
|
||||||
identityPath?: string;
|
identityPath?: string;
|
||||||
userPath?: string;
|
userPath?: string;
|
||||||
|
heartbeatPath?: string;
|
||||||
bootstrapPath?: string;
|
bootstrapPath?: string;
|
||||||
}> {
|
}> {
|
||||||
const rawDir = params?.dir?.trim()
|
const rawDir = params?.dir?.trim()
|
||||||
@@ -220,10 +232,18 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
||||||
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
||||||
const userPath = path.join(dir, DEFAULT_USER_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 bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
||||||
|
|
||||||
const isBrandNewWorkspace = await (async () => {
|
const isBrandNewWorkspace = await (async () => {
|
||||||
const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath];
|
const paths = [
|
||||||
|
agentsPath,
|
||||||
|
soulPath,
|
||||||
|
toolsPath,
|
||||||
|
identityPath,
|
||||||
|
userPath,
|
||||||
|
heartbeatPath,
|
||||||
|
];
|
||||||
const existing = await Promise.all(
|
const existing = await Promise.all(
|
||||||
paths.map(async (p) => {
|
paths.map(async (p) => {
|
||||||
try {
|
try {
|
||||||
@@ -257,6 +277,10 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
DEFAULT_USER_FILENAME,
|
DEFAULT_USER_FILENAME,
|
||||||
DEFAULT_USER_TEMPLATE,
|
DEFAULT_USER_TEMPLATE,
|
||||||
);
|
);
|
||||||
|
const heartbeatTemplate = await loadTemplate(
|
||||||
|
DEFAULT_HEARTBEAT_FILENAME,
|
||||||
|
DEFAULT_HEARTBEAT_TEMPLATE,
|
||||||
|
);
|
||||||
const bootstrapTemplate = await loadTemplate(
|
const bootstrapTemplate = await loadTemplate(
|
||||||
DEFAULT_BOOTSTRAP_FILENAME,
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
DEFAULT_BOOTSTRAP_TEMPLATE,
|
DEFAULT_BOOTSTRAP_TEMPLATE,
|
||||||
@@ -267,6 +291,7 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
await writeFileIfMissing(toolsPath, toolsTemplate);
|
await writeFileIfMissing(toolsPath, toolsTemplate);
|
||||||
await writeFileIfMissing(identityPath, identityTemplate);
|
await writeFileIfMissing(identityPath, identityTemplate);
|
||||||
await writeFileIfMissing(userPath, userTemplate);
|
await writeFileIfMissing(userPath, userTemplate);
|
||||||
|
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
||||||
if (isBrandNewWorkspace) {
|
if (isBrandNewWorkspace) {
|
||||||
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
||||||
}
|
}
|
||||||
@@ -278,6 +303,7 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
toolsPath,
|
toolsPath,
|
||||||
identityPath,
|
identityPath,
|
||||||
userPath,
|
userPath,
|
||||||
|
heartbeatPath,
|
||||||
bootstrapPath,
|
bootstrapPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -311,6 +337,10 @@ export async function loadWorkspaceBootstrapFiles(
|
|||||||
name: DEFAULT_USER_FILENAME,
|
name: DEFAULT_USER_FILENAME,
|
||||||
filePath: path.join(resolvedDir, 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,
|
name: DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME),
|
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME),
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
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 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";
|
export type StripHeartbeatMode = "heartbeat" | "message";
|
||||||
|
|
||||||
function stripTokenAtEdges(raw: string): { text: string; didStrip: boolean } {
|
function stripTokenAtEdges(raw: string): { text: string; didStrip: boolean } {
|
||||||
|
|||||||
@@ -853,7 +853,7 @@ export type ClawdbotConfig = {
|
|||||||
typingIntervalSeconds?: number;
|
typingIntervalSeconds?: number;
|
||||||
/** Periodic background heartbeat runs. */
|
/** Periodic background heartbeat runs. */
|
||||||
heartbeat?: {
|
heartbeat?: {
|
||||||
/** Heartbeat interval (duration string, default unit: minutes). */
|
/** Heartbeat interval (duration string, default unit: minutes; default: 30m). */
|
||||||
every?: string;
|
every?: string;
|
||||||
/** Heartbeat model override (provider/model). */
|
/** Heartbeat model override (provider/model). */
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -869,7 +869,7 @@ export type ClawdbotConfig = {
|
|||||||
| "none";
|
| "none";
|
||||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||||
to?: string;
|
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;
|
prompt?: string;
|
||||||
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||||
ackMaxChars?: number;
|
ackMaxChars?: number;
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import {
|
|||||||
} from "./heartbeat-runner.js";
|
} from "./heartbeat-runner.js";
|
||||||
|
|
||||||
describe("resolveHeartbeatIntervalMs", () => {
|
describe("resolveHeartbeatIntervalMs", () => {
|
||||||
it("returns null when unset or invalid", () => {
|
it("returns default when unset", () => {
|
||||||
expect(resolveHeartbeatIntervalMs({})).toBeNull();
|
expect(resolveHeartbeatIntervalMs({})).toBe(30 * 60_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when invalid or zero", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "0m" } } }),
|
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "0m" } } }),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
HEARTBEAT_PROMPT,
|
DEFAULT_HEARTBEAT_EVERY,
|
||||||
|
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
} from "../auto-reply/heartbeat.js";
|
} from "../auto-reply/heartbeat.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
@@ -83,7 +84,8 @@ export function resolveHeartbeatIntervalMs(
|
|||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
overrideEvery?: string,
|
overrideEvery?: string,
|
||||||
) {
|
) {
|
||||||
const raw = overrideEvery ?? cfg.agent?.heartbeat?.every;
|
const raw =
|
||||||
|
overrideEvery ?? cfg.agent?.heartbeat?.every ?? DEFAULT_HEARTBEAT_EVERY;
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const trimmed = String(raw).trim();
|
const trimmed = String(raw).trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
@@ -98,9 +100,7 @@ export function resolveHeartbeatIntervalMs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) {
|
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) {
|
||||||
const raw = cfg.agent?.heartbeat?.prompt;
|
return resolveHeartbeatPromptText(cfg.agent?.heartbeat?.prompt);
|
||||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
||||||
return trimmed || HEARTBEAT_PROMPT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) {
|
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
HEARTBEAT_PROMPT,
|
HEARTBEAT_PROMPT,
|
||||||
|
resolveHeartbeatPrompt,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
} from "../auto-reply/heartbeat.js";
|
} from "../auto-reply/heartbeat.js";
|
||||||
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
||||||
@@ -339,7 +340,7 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
|
|
||||||
const replyResult = await replyResolver(
|
const replyResult = await replyResolver(
|
||||||
{
|
{
|
||||||
Body: HEARTBEAT_PROMPT,
|
Body: resolveHeartbeatPrompt(cfg.agent?.heartbeat?.prompt),
|
||||||
From: to,
|
From: to,
|
||||||
To: to,
|
To: to,
|
||||||
MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
|
MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
|
||||||
|
|||||||
Reference in New Issue
Block a user