mirror of https://github.com/openclaw/openclaw.git
Heartbeat: add isolatedSession option for fresh session per heartbeat run (#46634)
Reuses the cron isolated session pattern (resolveCronSession with forceNew) to give each heartbeat a fresh session with no prior conversation history. Reduces per-heartbeat token cost from ~100K to ~2-5K tokens. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e8df16732
commit
2806f2b878
|
|
@ -1484,6 +1484,16 @@
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"hasChildren": false
|
"hasChildren": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "agents.defaults.heartbeat.isolatedSession",
|
||||||
|
"kind": "core",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"deprecated": false,
|
||||||
|
"sensitive": false,
|
||||||
|
"tags": [],
|
||||||
|
"hasChildren": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "agents.defaults.heartbeat.lightContext",
|
"path": "agents.defaults.heartbeat.lightContext",
|
||||||
"kind": "core",
|
"kind": "core",
|
||||||
|
|
@ -1544,7 +1554,7 @@
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"sensitive": false,
|
"sensitive": false,
|
||||||
"tags": ["automation"],
|
"tags": ["automation"],
|
||||||
"help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.",
|
"help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.",
|
||||||
"hasChildren": false
|
"hasChildren": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -3647,6 +3657,16 @@
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"hasChildren": false
|
"hasChildren": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "agents.list.*.heartbeat.isolatedSession",
|
||||||
|
"kind": "core",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"deprecated": false,
|
||||||
|
"sensitive": false,
|
||||||
|
"tags": [],
|
||||||
|
"hasChildren": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "agents.list.*.heartbeat.lightContext",
|
"path": "agents.list.*.heartbeat.lightContext",
|
||||||
"kind": "core",
|
"kind": "core",
|
||||||
|
|
@ -3707,7 +3727,7 @@
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"sensitive": false,
|
"sensitive": false,
|
||||||
"tags": ["automation"],
|
"tags": ["automation"],
|
||||||
"help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.",
|
"help": "Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.",
|
||||||
"hasChildren": false
|
"hasChildren": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":4731}
|
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":4733}
|
||||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
|
|
@ -137,12 +137,13 @@
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.directPolicy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["access","automation","storage"],"label":"Heartbeat Direct Policy","help":"Controls whether heartbeat delivery may target direct/DM chats: \"allow\" (default) permits DM delivery and \"block\" suppresses direct-target sends.","hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.directPolicy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["access","automation","storage"],"label":"Heartbeat Direct Policy","help":"Controls whether heartbeat delivery may target direct/DM chats: \"allow\" (default) permits DM delivery and \"block\" suppresses direct-target sends.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.every","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.every","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.includeReasoning","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.includeReasoning","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
|
{"recordType":"path","path":"agents.defaults.heartbeat.isolatedSession","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.lightContext","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.lightContext","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.","hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.defaults.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
{"recordType":"path","path":"agents.defaults.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||||
{"recordType":"path","path":"agents.defaults.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Human Delay Max (ms)","help":"Maximum delay in ms for custom humanDelay (default: 2500).","hasChildren":false}
|
{"recordType":"path","path":"agents.defaults.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Human Delay Max (ms)","help":"Maximum delay in ms for custom humanDelay (default: 2500).","hasChildren":false}
|
||||||
|
|
@ -340,12 +341,13 @@
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.directPolicy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["access","automation","storage"],"label":"Heartbeat Direct Policy","help":"Per-agent override for heartbeat direct/DM delivery policy; use \"block\" for agents that should only send heartbeat alerts to non-DM destinations.","hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.directPolicy","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["access","automation","storage"],"label":"Heartbeat Direct Policy","help":"Per-agent override for heartbeat direct/DM delivery policy; use \"block\" for agents that should only send heartbeat alerts to non-DM destinations.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.every","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.every","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.includeReasoning","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.includeReasoning","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
|
{"recordType":"path","path":"agents.list.*.heartbeat.isolatedSession","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.lightContext","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.lightContext","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Agent Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Agent Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.","hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.","hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
{"recordType":"path","path":"agents.list.*.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
{"recordType":"path","path":"agents.list.*.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||||
{"recordType":"path","path":"agents.list.*.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
{"recordType":"path","path":"agents.list.*.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||||
|
|
|
||||||
|
|
@ -975,6 +975,7 @@ Periodic heartbeat runs.
|
||||||
model: "openai/gpt-5.2-mini",
|
model: "openai/gpt-5.2-mini",
|
||||||
includeReasoning: false,
|
includeReasoning: false,
|
||||||
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
||||||
|
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
|
||||||
session: "main",
|
session: "main",
|
||||||
to: "+15555550123",
|
to: "+15555550123",
|
||||||
directPolicy: "allow", // allow (default) | block
|
directPolicy: "allow", // allow (default) | block
|
||||||
|
|
@ -992,6 +993,7 @@ Periodic heartbeat runs.
|
||||||
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
|
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
|
||||||
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
|
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
|
||||||
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
||||||
|
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Same isolation pattern as cron `sessionTarget: "isolated"`. Reduces per-heartbeat token cost from ~100K to ~2-5K tokens.
|
||||||
- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
|
- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
|
||||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
|
||||||
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
|
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
|
||||||
4. Optional: enable heartbeat reasoning delivery for transparency.
|
4. Optional: enable heartbeat reasoning delivery for transparency.
|
||||||
5. Optional: use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
|
5. Optional: use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
|
||||||
6. Optional: restrict heartbeats to active hours (local time).
|
6. Optional: enable isolated sessions to avoid sending full conversation history each heartbeat.
|
||||||
|
7. Optional: restrict heartbeats to active hours (local time).
|
||||||
|
|
||||||
Example config:
|
Example config:
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ Example config:
|
||||||
target: "last", // explicit delivery to last contact (default is "none")
|
target: "last", // explicit delivery to last contact (default is "none")
|
||||||
directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
|
directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
|
||||||
lightContext: true, // optional: only inject HEARTBEAT.md from bootstrap files
|
lightContext: true, // optional: only inject HEARTBEAT.md from bootstrap files
|
||||||
|
isolatedSession: true, // optional: fresh session each run (no conversation history)
|
||||||
// activeHours: { start: "08:00", end: "24:00" },
|
// activeHours: { start: "08:00", end: "24:00" },
|
||||||
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
||||||
},
|
},
|
||||||
|
|
@ -91,6 +93,7 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
||||||
model: "anthropic/claude-opus-4-6",
|
model: "anthropic/claude-opus-4-6",
|
||||||
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
||||||
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
||||||
|
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
|
||||||
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
||||||
to: "+15551234567", // optional channel-specific override
|
to: "+15551234567", // optional channel-specific override
|
||||||
accountId: "ops-bot", // optional multi-account channel id
|
accountId: "ops-bot", // optional multi-account channel id
|
||||||
|
|
@ -212,6 +215,7 @@ Use `accountId` to target a specific account on multi-account channels like Tele
|
||||||
- `model`: optional model override for heartbeat runs (`provider/model`).
|
- `model`: optional model override for heartbeat runs (`provider/model`).
|
||||||
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
|
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
|
||||||
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
||||||
|
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Uses the same isolation pattern as cron `sessionTarget: "isolated"`. Dramatically reduces per-heartbeat token cost. Combine with `lightContext: true` for maximum savings. Delivery routing still uses the main session context.
|
||||||
- `session`: optional session key for heartbeat runs.
|
- `session`: optional session key for heartbeat runs.
|
||||||
- `main` (default): agent main session.
|
- `main` (default): agent main session.
|
||||||
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
|
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
|
||||||
|
|
@ -380,6 +384,10 @@ off in group chats.
|
||||||
|
|
||||||
## Cost awareness
|
## Cost awareness
|
||||||
|
|
||||||
Heartbeats run full agent turns. Shorter intervals burn more tokens. Keep
|
Heartbeats run full agent turns. Shorter intervals burn more tokens. To reduce cost:
|
||||||
`HEARTBEAT.md` small and consider a cheaper `model` or `target: "none"` if you
|
|
||||||
only want internal state updates.
|
- Use `isolatedSession: true` to avoid sending full conversation history (~100K tokens down to ~2-5K per run).
|
||||||
|
- Use `lightContext: true` to limit bootstrap files to just `HEARTBEAT.md`.
|
||||||
|
- Set a cheaper `model` (e.g. `ollama/llama3.2:1b`).
|
||||||
|
- Keep `HEARTBEAT.md` small.
|
||||||
|
- Use `target: "none"` if you only want internal state updates.
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const AGENT_HEARTBEAT_KEYS = new Set([
|
||||||
"ackMaxChars",
|
"ackMaxChars",
|
||||||
"suppressToolErrorWarnings",
|
"suppressToolErrorWarnings",
|
||||||
"lightContext",
|
"lightContext",
|
||||||
|
"isolatedSession",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]);
|
const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]);
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,13 @@ export type AgentDefaultsConfig = {
|
||||||
* Lightweight mode keeps only HEARTBEAT.md from workspace bootstrap files.
|
* Lightweight mode keeps only HEARTBEAT.md from workspace bootstrap files.
|
||||||
*/
|
*/
|
||||||
lightContext?: boolean;
|
lightContext?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, run heartbeat turns in an isolated session with no prior
|
||||||
|
* conversation history. The heartbeat only sees its bootstrap context
|
||||||
|
* (HEARTBEAT.md when lightContext is also enabled). Dramatically reduces
|
||||||
|
* per-heartbeat token cost by avoiding the full session transcript.
|
||||||
|
*/
|
||||||
|
isolatedSession?: boolean;
|
||||||
/**
|
/**
|
||||||
* When enabled, deliver the model's reasoning payload for heartbeat runs (when available)
|
* When enabled, deliver the model's reasoning payload for heartbeat runs (when available)
|
||||||
* as a separate message prefixed with `Reasoning:` (same as `/reasoning on`).
|
* as a separate message prefixed with `Reasoning:` (same as `/reasoning on`).
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export const HeartbeatSchema = z
|
||||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||||
suppressToolErrorWarnings: z.boolean().optional(),
|
suppressToolErrorWarnings: z.boolean().optional(),
|
||||||
lightContext: z.boolean().optional(),
|
lightContext: z.boolean().optional(),
|
||||||
|
isolatedSession: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.superRefine((val, ctx) => {
|
.superRefine((val, ctx) => {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||||
model?: string;
|
model?: string;
|
||||||
suppressToolErrorWarnings?: boolean;
|
suppressToolErrorWarnings?: boolean;
|
||||||
lightContext?: boolean;
|
lightContext?: boolean;
|
||||||
|
isolatedSession?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
return withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
|
|
@ -77,6 +78,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||||
model: params.model,
|
model: params.model,
|
||||||
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
|
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
|
||||||
lightContext: params.lightContext,
|
lightContext: params.lightContext,
|
||||||
|
isolatedSession: params.isolatedSession,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -133,6 +135,72 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses isolated session key when isolatedSession is enabled", async () => {
|
||||||
|
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
workspace: tmpDir,
|
||||||
|
heartbeat: {
|
||||||
|
every: "5m",
|
||||||
|
target: "whatsapp",
|
||||||
|
isolatedSession: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||||
|
session: { store: storePath },
|
||||||
|
};
|
||||||
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
await seedSession(sessionKey, { lastChannel: "whatsapp", lastTo: "+1555" });
|
||||||
|
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
||||||
|
|
||||||
|
await runHeartbeatOnce({
|
||||||
|
cfg,
|
||||||
|
deps: { getQueueSize: () => 0, nowMs: () => 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const ctx = replySpy.mock.calls[0]?.[0];
|
||||||
|
// Isolated heartbeat runs use a dedicated session key with :heartbeat suffix
|
||||||
|
expect(ctx.SessionKey).toBe(`${sessionKey}:heartbeat`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses main session key when isolatedSession is not set", async () => {
|
||||||
|
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
workspace: tmpDir,
|
||||||
|
heartbeat: {
|
||||||
|
every: "5m",
|
||||||
|
target: "whatsapp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||||
|
session: { store: storePath },
|
||||||
|
};
|
||||||
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
await seedSession(sessionKey, { lastChannel: "whatsapp", lastTo: "+1555" });
|
||||||
|
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
||||||
|
|
||||||
|
await runHeartbeatOnce({
|
||||||
|
cfg,
|
||||||
|
deps: { getQueueSize: () => 0, nowMs: () => 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const ctx = replySpy.mock.calls[0]?.[0];
|
||||||
|
expect(ctx.SessionKey).toBe(sessionKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("passes per-agent heartbeat model override (merged with defaults)", async () => {
|
it("passes per-agent heartbeat model override (merged with defaults)", async () => {
|
||||||
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
|
||||||
|
import { resolveCronSession } from "../cron/isolated-agent/session.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { getQueueSize } from "../process/command-queue.js";
|
import { getQueueSize } from "../process/command-queue.js";
|
||||||
import { CommandLane } from "../process/lanes.js";
|
import { CommandLane } from "../process/lanes.js";
|
||||||
|
|
@ -659,6 +660,30 @@ export async function runHeartbeatOnce(opts: {
|
||||||
}
|
}
|
||||||
const { entry, sessionKey, storePath } = preflight.session;
|
const { entry, sessionKey, storePath } = preflight.session;
|
||||||
const previousUpdatedAt = entry?.updatedAt;
|
const previousUpdatedAt = entry?.updatedAt;
|
||||||
|
|
||||||
|
// When isolatedSession is enabled, create a fresh session via the same
|
||||||
|
// pattern as cron sessionTarget: "isolated". This gives the heartbeat
|
||||||
|
// a new session ID (empty transcript) each run, avoiding the cost of
|
||||||
|
// sending the full conversation history (~100K tokens) to the LLM.
|
||||||
|
// Delivery routing still uses the main session entry (lastChannel, lastTo).
|
||||||
|
const useIsolatedSession = heartbeat?.isolatedSession === true;
|
||||||
|
let runSessionKey = sessionKey;
|
||||||
|
let runStorePath = storePath;
|
||||||
|
if (useIsolatedSession) {
|
||||||
|
const isolatedKey = `${sessionKey}:heartbeat`;
|
||||||
|
const cronSession = resolveCronSession({
|
||||||
|
cfg,
|
||||||
|
sessionKey: isolatedKey,
|
||||||
|
agentId,
|
||||||
|
nowMs: startedAt,
|
||||||
|
forceNew: true,
|
||||||
|
});
|
||||||
|
cronSession.store[isolatedKey] = cronSession.sessionEntry;
|
||||||
|
await saveSessionStore(cronSession.storePath, cronSession.store);
|
||||||
|
runSessionKey = isolatedKey;
|
||||||
|
runStorePath = cronSession.storePath;
|
||||||
|
}
|
||||||
|
|
||||||
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
|
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
|
||||||
const heartbeatAccountId = heartbeat?.accountId?.trim();
|
const heartbeatAccountId = heartbeat?.accountId?.trim();
|
||||||
if (delivery.reason === "unknown-account") {
|
if (delivery.reason === "unknown-account") {
|
||||||
|
|
@ -707,7 +732,7 @@ export async function runHeartbeatOnce(opts: {
|
||||||
AccountId: delivery.accountId,
|
AccountId: delivery.accountId,
|
||||||
MessageThreadId: delivery.threadId,
|
MessageThreadId: delivery.threadId,
|
||||||
Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat",
|
Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat",
|
||||||
SessionKey: sessionKey,
|
SessionKey: runSessionKey,
|
||||||
};
|
};
|
||||||
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
|
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
|
||||||
emitHeartbeatEvent({
|
emitHeartbeatEvent({
|
||||||
|
|
@ -758,10 +783,11 @@ export async function runHeartbeatOnce(opts: {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Capture transcript state before the heartbeat run so we can prune if HEARTBEAT_OK
|
// Capture transcript state before the heartbeat run so we can prune if HEARTBEAT_OK.
|
||||||
|
// For isolated sessions, capture the isolated transcript (not the main session's).
|
||||||
const transcriptState = await captureTranscriptState({
|
const transcriptState = await captureTranscriptState({
|
||||||
storePath,
|
storePath: runStorePath,
|
||||||
sessionKey,
|
sessionKey: runSessionKey,
|
||||||
agentId,
|
agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue