fix(gateway): stop showing retired HEARTBEAT.md in the agent files editor (#113621)

* fix(gateway): drop retired HEARTBEAT.md from agent core files

* fix(ci): stop asserting retired HEARTBEAT.md in docker release smoke

* docs: align HEARTBEAT.md references with cron scratch

* docs(agents): drop retired HEARTBEAT.md from workspace read comment
This commit is contained in:
Peter Steinberger
2026-07-25 04:58:31 -07:00
committed by GitHub
parent 591e029095
commit f43fac21c7
30 changed files with 77 additions and 62 deletions
-2
View File
@@ -359,7 +359,6 @@ jobs:
cat "${temp_root}/out.log"
exit 1
fi
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
if [ "${status}" -ne 0 ]; then
cat "${temp_root}/out.log"
fi
@@ -568,7 +567,6 @@ jobs:
cat "${temp_root}/out.log"
exit 1
fi
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
if [ "${status}" -ne 0 ]; then
cat "${temp_root}/out.log"
fi
+8
View File
@@ -1634,5 +1634,13 @@
{
"source": "Gateway security",
"target": "Gateway 安全"
},
{
"source": "Retired HEARTBEAT.md workspace file",
"target": "已弃用的 HEARTBEAT.md 工作区文件"
},
{
"source": "Cron CLI",
"target": "Cron CLI"
}
]
+1 -1
View File
@@ -259,7 +259,7 @@ Extracts the last user/assistant messages (default 15, configurable with `hooks.
}
```
`patterns` and `files` are accepted as aliases of `paths`. Paths resolve relative to the workspace and must stay inside it. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`).
`patterns` and `files` are accepted as aliases of `paths`. Paths resolve relative to the workspace and must stay inside it. Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `BOOTSTRAP.md`, `MEMORY.md`).
<a id="command-logger"></a>
+1 -1
View File
@@ -29,7 +29,7 @@ Each program specifies:
The agent loads these instructions every session via the workspace bootstrap files (see [Agent Workspace](/concepts/agent-workspace) for the full list of auto-injected files) and executes against them, combined with [cron jobs](/automation/cron-jobs) for time-based enforcement.
<Tip>
Put standing orders in `AGENTS.md` to guarantee they're loaded every session. The workspace bootstrap automatically injects `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, and `MEMORY.md` - but not arbitrary files in subdirectories.
Put standing orders in `AGENTS.md` to guarantee they're loaded every session. The workspace bootstrap automatically injects `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `BOOTSTRAP.md`, and `MEMORY.md` - but not arbitrary files in subdirectories.
</Tip>
## Anatomy of a standing order
+1 -1
View File
@@ -291,7 +291,7 @@ openclaw onboard --non-interactive \
- Unless you pass `--skip-health`, onboarding waits for a reachable local gateway before exiting successfully.
- `--install-daemon` starts the managed gateway install path first. Without it, a local gateway must already be running (for example `openclaw gateway run`).
- `--skip-health` skips the wait if you only want config/workspace/bootstrap writes in automation.
- `--skip-bootstrap` sets `agents.defaults.skipBootstrap: true` and skips creating `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`.
- `--skip-bootstrap` sets `agents.defaults.skipBootstrap: true` and skips creating `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`.
- On native Windows, `--install-daemon` tries Scheduled Tasks first and falls back to a per-user Startup-folder login item if task creation is denied.
### Interactive ref mode
+2 -5
View File
@@ -79,9 +79,6 @@ Standard files OpenClaw expects inside the workspace:
<Accordion title="TOOLS.md - local tool conventions">
Notes about your local tools and conventions. Does not control tool availability; it is only guidance.
</Accordion>
<Accordion title="HEARTBEAT.md - heartbeat checklist">
Optional tiny checklist for heartbeat runs. Keep it short to avoid token burn.
</Accordion>
<Accordion title="BOOT.md - startup checklist">
Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled). Keep it short; use the message tool for outbound sends.
</Accordion>
@@ -140,7 +137,7 @@ Run these steps on the machine where the Gateway runs (that is where the workspa
```bash
cd ~/.openclaw/workspace
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md memory/
git commit -m "Add agent workspace"
```
@@ -238,7 +235,7 @@ Suggested `.gitignore` starter:
## Related
- [Heartbeat](/gateway/heartbeat) - HEARTBEAT.md workspace file
- [Heartbeat](/gateway/heartbeat) - heartbeat monitors and cron scratch
- [Sandboxing](/gateway/sandboxing) - workspace access in sandboxed environments
- [Session](/concepts/session) - session storage paths
- [Standing orders](/automation/standing-orders) - persistent instructions in workspace files
-1
View File
@@ -37,7 +37,6 @@ Inside the workspace, OpenClaw expects these user-editable files:
| `TOOLS.md` | User-maintained tool notes and conventions |
| `IDENTITY.md` | Agent name/vibe/emoji |
| `USER.md` | User profile + preferred address |
| `HEARTBEAT.md` | Heartbeat-specific instructions |
| `BOOTSTRAP.md` | One-time first-run ritual (deleted after completion) |
| `MEMORY.md` | Root long-term memory file, if present |
-2
View File
@@ -47,7 +47,6 @@ Injected workspace files:
- TOOLS.md: TRUNCATED | raw 54,210 chars (~13,553 tok) | injected 20,962 chars (~5,241 tok)
- IDENTITY.md: OK | raw 211 chars (~53 tok) | injected 211 chars (~53 tok)
- USER.md: OK | raw 388 chars (~97 tok) | injected 388 chars (~97 tok)
- HEARTBEAT.md: MISSING | raw 0 | injected 0
- BOOTSTRAP.md: OK | raw 0 chars (~0 tok) | injected 0 chars (~0 tok)
Skills list (system prompt text): 2,184 chars (~546 tok) (12 skills)
@@ -122,7 +121,6 @@ By default, OpenClaw injects a fixed set of workspace files (if present):
- `TOOLS.md`
- `IDENTITY.md`
- `USER.md`
- `HEARTBEAT.md`
- `BOOTSTRAP.md` (first-run only)
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `60000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
+2 -3
View File
@@ -94,13 +94,12 @@ Bootstrap files are resolved from the active workspace and routed to the prompt
- `TOOLS.md`
- `IDENTITY.md`
- `USER.md`
- `HEARTBEAT.md`
- `BOOTSTRAP.md` (only on brand-new workspaces)
- `MEMORY.md` when present
On the native Codex harness, OpenClaw avoids repeating stable workspace files in every user turn. Codex loads `AGENTS.md` through its own project-doc discovery. `TOOLS.md` is forwarded as inherited Codex developer instructions. `SOUL.md`, `IDENTITY.md`, and `USER.md` are forwarded as turn-scoped collaboration developer instructions so native Codex sub-agents do not inherit them. `HEARTBEAT.md` content is not injected directly; heartbeat turns get a collaboration-mode note pointing to the file when it exists and is non-empty. `MEMORY.md` content is not pasted into every native Codex turn either: when memory tools are available for the workspace, Codex turns get a small workspace-memory note directing the model to `memory_search` or `memory_get`. If tools are disabled, memory search is unavailable, or the active workspace differs from the agent memory workspace, `MEMORY.md` falls back to the normal bounded turn-context path. `BOOTSTRAP.md` keeps the normal turn-context role.
On the native Codex harness, OpenClaw avoids repeating stable workspace files in every user turn. Codex loads `AGENTS.md` through its own project-doc discovery. `TOOLS.md` is forwarded as inherited Codex developer instructions. `SOUL.md`, `IDENTITY.md`, and `USER.md` are forwarded as turn-scoped collaboration developer instructions so native Codex sub-agents do not inherit them. `MEMORY.md` content is not pasted into every native Codex turn either: when memory tools are available for the workspace, Codex turns get a small workspace-memory note directing the model to `memory_search` or `memory_get`. If tools are disabled, memory search is unavailable, or the active workspace differs from the agent memory workspace, `MEMORY.md` falls back to the normal bounded turn-context path. `BOOTSTRAP.md` keeps the normal turn-context role.
On non-Codex harnesses, bootstrap files compose into the OpenClaw prompt per their existing gates. `HEARTBEAT.md` is omitted on normal runs when heartbeats are disabled for the default agent or `agents.defaults.heartbeat.includeSystemPromptSection` is false. Keep injected files concise, especially non-Codex `MEMORY.md`: it should stay a curated long-term summary, with detailed daily notes in `memory/*.md` retrievable on demand via `memory_search` / `memory_get`. Oversized non-Codex `MEMORY.md` files increase prompt usage and can be partially injected under the bootstrap file limits below.
On non-Codex harnesses, bootstrap files compose into the OpenClaw prompt per their existing gates. Keep injected files concise, especially non-Codex `MEMORY.md`: it should stay a curated long-term summary, with detailed daily notes in `memory/*.md` retrievable on demand via `memory_search` / `memory_get`. Oversized non-Codex `MEMORY.md` files increase prompt usage and can be partially injected under the bootstrap file limits below.
<Note>
`memory/*.md` daily files are **not** part of the normal bootstrap Project Context. On ordinary turns they are accessed on demand via `memory_search` / `memory_get`, so they do not count against the context window unless the model explicitly reads them. Bare `/new` and `/reset` turns are the exception: the runtime can prepend recent daily memory as a one-shot startup-context block for that first turn.
+1 -1
View File
@@ -9057,7 +9057,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- Route: /reference/templates/HEARTBEAT
- Headings:
- H1: HEARTBEAT.md template
- H1: HEARTBEAT.md is retired
- H2: Related
## reference/templates/IDENTITY.dev.md
+2 -2
View File
@@ -477,7 +477,7 @@ First-run Q&A - install, onboard, auth routes, subscriptions, initial failures -
<Accordion title="Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?">
These live in the **agent workspace**, not `~/.openclaw`.
- **Workspace (per agent)**: `AGENTS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md`, `memory/YYYY-MM-DD.md`, optional `HEARTBEAT.md`. Lowercase root `memory.md` is legacy repair input only; `openclaw doctor --fix` can merge it into `MEMORY.md` when both exist.
- **Workspace (per agent)**: `AGENTS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md`, `memory/YYYY-MM-DD.md`. Lowercase root `memory.md` is legacy repair input only; `openclaw doctor --fix` can merge it into `MEMORY.md` when both exist.
- **State dir (`~/.openclaw`)**: config, channel/provider state, auth profiles, sessions, logs, shared skills (`~/.openclaw/skills`).
Default workspace is `~/.openclaw/workspace`, configurable:
@@ -1036,7 +1036,7 @@ First-run Q&A - install, onboard, auth routes, subscriptions, initial failures -
}
```
If `HEARTBEAT.md` exists but is effectively empty (only blank lines, Markdown/HTML comments, ATX headings, fence markers, or empty list-item stubs), OpenClaw skips the heartbeat run to save API calls. If the file is missing, the heartbeat still runs and the model decides what to do.
Heartbeat instructions live in the monitor's cron scratch. Effectively empty scratch skips the heartbeat run to save API calls; without scratch, the heartbeat still runs and the model decides what to do.
Per-agent overrides use `agents.entries.*.heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
+3 -3
View File
@@ -685,9 +685,9 @@ bootstrap files as developer instructions, but not identically:
- The compact loaded OpenClaw skills list is also forwarded as turn-scoped
collaboration developer instructions, so native Codex subagents do not
inherit it either.
- `HEARTBEAT.md` content is not injected; heartbeat turns get a
collaboration-mode pointer to read the file when it exists and is
non-empty.
- Heartbeat turns receive generic initiative guidance through collaboration
mode. Monitor cron scratch is appended to the heartbeat prompt instead of
injected as workspace context.
- `MEMORY.md` content from the configured agent workspace is not pasted into
native Codex turn input when memory tools are available for that
workspace; when it exists, the harness adds a small workspace-memory
+2 -2
View File
@@ -124,8 +124,8 @@ Codex heartbeat turns get `heartbeat_respond` in the searchable OpenClaw tool
catalog by default so the agent can record whether the wake should stay quiet
or notify. Heartbeat initiative guidance is sent as a Codex collaboration-mode
developer instruction scoped to the heartbeat turn; ordinary chat turns stay
in Codex Default mode. When `HEARTBEAT.md` is non-empty, the heartbeat
instructions point Codex at the file instead of inlining its contents.
in Codex Default mode. The heartbeat monitor's cron scratch is appended to the
heartbeat prompt when present.
## Hook boundaries
+2 -2
View File
@@ -129,11 +129,11 @@ If a provider does not support any of the above cache modes, `cacheRetention` ha
## System-prompt cache boundary
OpenClaw splits the system prompt into a **stable prefix** and a **volatile suffix** at an internal cache-prefix boundary. Content above the boundary (tool definitions, skills metadata, workspace files) is ordered to stay byte-identical across turns. Content below the boundary (for example `HEARTBEAT.md`, runtime timestamps, other per-turn metadata) can change without invalidating the cached prefix.
OpenClaw splits the system prompt into a **stable prefix** and a **volatile suffix** at an internal cache-prefix boundary. Content above the boundary (tool definitions, skills metadata, workspace files) is ordered to stay byte-identical across turns. Content below the boundary (for example runtime timestamps and other per-turn metadata) can change without invalidating the cached prefix.
Key design choices:
- Stable workspace project-context files are ordered before `HEARTBEAT.md` so heartbeat churn does not bust the stable prefix.
- Stable workspace project-context files are ordered before volatile per-turn metadata so routine churn does not bust the stable prefix.
- The boundary applies across Anthropic-family, OpenAI-family, Google, and CLI transport shaping, so all supported providers benefit from the same prefix stability.
- Codex Responses and Anthropic Vertex requests are routed through boundary-aware cache shaping so cache reuse stays aligned with what providers actually receive.
- System-prompt fingerprints are normalized (whitespace, line endings, hook-added context, runtime capability ordering) so semantically unchanged prompts share cache across turns.
+1 -1
View File
@@ -51,7 +51,7 @@ Before proposing or building a custom system, feature, workflow, tool, integrati
## Heartbeats (optional)
- HEARTBEAT.md can hold a tiny checklist for heartbeat runs; keep it small.
- The heartbeat monitor's cron scratch can hold a tiny checklist for heartbeat runs; keep it small.
## Customize
+1 -1
View File
@@ -97,7 +97,7 @@ Skills provide your tools. When you need one, check its `SKILL.md`. Keep local n
## Heartbeats - Be Proactive
When you receive a heartbeat poll (message matches the configured heartbeat prompt), don't just reply `HEARTBEAT_OK` every time. You're free to edit `HEARTBEAT.md` with a short checklist or reminders - keep it small to limit token burn.
When you receive a heartbeat poll (message matches the configured heartbeat prompt), don't just reply `HEARTBEAT_OK` every time. Keep a short checklist or reminders in the heartbeat monitor's cron scratch; use `openclaw cron list --all` to find the monitor job, then `openclaw cron scratch <jobId> --set "..."` to update it. Keep it small to limit token burn.
See [Scheduled Tasks (Cron) vs Heartbeat](/automation#scheduled-tasks-cron-vs-heartbeat) for the full decision table. Short version: heartbeat batches periodic checks with full session context on approximate timing (default every 30 minutes); cron is for exact timing, isolated runs, a different model, or one-shot reminders.
+1 -1
View File
@@ -9,7 +9,7 @@ read_when:
_You just woke up. Keep this first conversation short and make it yours._
OpenClaw only seeds this file into a brand-new workspace, alongside `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `HEARTBEAT.md`. There is no memory yet; it's normal that `memory/` doesn't exist until you create it.
OpenClaw only seeds this file into a brand-new workspace, alongside `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, and `USER.md`. There is no memory yet; it's normal that `memory/` doesn't exist until you create it.
Complete these three beats. Do not turn them into a questionnaire or a long
biography.
+14 -15
View File
@@ -1,29 +1,28 @@
---
summary: "Workspace template for HEARTBEAT.md"
title: "HEARTBEAT.md template"
summary: "Migration guide for the retired HEARTBEAT.md workspace file"
title: "Retired HEARTBEAT.md workspace file"
read_when:
- Bootstrapping a workspace manually
- Migrating an older workspace that still has HEARTBEAT.md
---
# HEARTBEAT.md template
# HEARTBEAT.md is retired
`HEARTBEAT.md` lives in the agent workspace and holds the periodic heartbeat checklist. Keep it empty, or with only whitespace, Markdown comments, ATX headings, empty list stubs (`- `, `* [ ]`), or fence markers, to make OpenClaw skip the heartbeat model call entirely (`reason=empty-heartbeat-file`).
OpenClaw no longer creates `HEARTBEAT.md` in new workspaces or reads it at runtime. Heartbeat instructions now live in the system-owned monitor's cron scratch in the shared state database.
Shipped default content:
Manage the current scratch with the monitor job id from `openclaw cron list --all`:
```markdown
<!-- Heartbeat template; comments-only content prevents scheduled heartbeat API calls. -->
# Keep this file empty (or with only comments) to skip heartbeat API calls.
# Add a short checklist below when the heartbeat should inspect shared context.
```bash
openclaw cron scratch <jobId>
openclaw cron scratch <jobId> --set "..."
openclaw cron scratch <jobId> --file notes.md
openclaw cron scratch <jobId> --unset
```
Add a short checklist below the comment lines only when one heartbeat turn should inspect the items together. Keep it small: heartbeat runs read this file every tick (default every 30 minutes), so bloated instructions burn tokens on every wake.
For independently scheduled or due-only checks, create [cron jobs](/automation/cron-jobs). Heartbeat scratch no longer supports scheduler syntax. Run `openclaw doctor --fix` to convert older `tasks:` blocks.
If an older workspace still contains `HEARTBEAT.md`, run `openclaw doctor --fix`. Doctor imports its instructions into monitor scratch, converts valid legacy `tasks:` entries into cron jobs, archives the original under the state directory, and removes the workspace file.
## Related
- [Heartbeat](/gateway/heartbeat)
- [Cron CLI](/cli/cron)
- [Doctor](/cli/doctor)
- [Heartbeat config](/gateway/config-agents)
+1 -1
View File
@@ -21,7 +21,7 @@ OpenClaw assembles its own system prompt on every run. It includes:
override at `agents.entries.*.skillsLimits.maxSkillsPromptChars`.
- Self-update instructions
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`,
`IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus
`IDENTITY.md`, `USER.md`, `BOOTSTRAP.md` when new, plus
`MEMORY.md` when present). Large injected files are truncated by
`agents.defaults.bootstrapMaxChars` (default: `20000`); total bootstrap
injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default:
+1 -1
View File
@@ -17,7 +17,7 @@ onboarding, on the agent's first real turn.
On the first run against a brand-new workspace (default `~/.openclaw/workspace`),
OpenClaw:
- Seeds `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`.
- Seeds `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`.
- Has the agent follow a capped three-beat birth sequence: it asks what you want
to call it, shares one short soul/vibe line, and asks whether you want the
minimal recommended plugin set or maximum convenience.
+1 -1
View File
@@ -180,7 +180,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Default AGENTS](/reference/AGENTS.default)
- [Templates: AGENTS](/reference/templates/AGENTS)
- [Templates: BOOTSTRAP](/reference/templates/BOOTSTRAP)
- [Templates: HEARTBEAT](/reference/templates/HEARTBEAT)
- [Retired HEARTBEAT.md workspace file](/reference/templates/HEARTBEAT)
- [Templates: IDENTITY](/reference/templates/IDENTITY)
- [Templates: SOUL](/reference/templates/SOUL)
- [Templates: TOOLS](/reference/templates/TOOLS)
+1 -1
View File
@@ -674,7 +674,7 @@ still need normal device approval for scope upgrades.
- Sub-agent announce is **best-effort**. If the gateway restarts, pending "announce back" work is lost.
- Sub-agents still share the same gateway process resources; treat `maxConcurrent` as a safety valve.
- `sessions_spawn` is always non-blocking: it returns `{ status: "accepted", runId, childSessionKey }` immediately.
- Sub-agent context only injects `AGENTS.md` and `TOOLS.md` (no `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md`, `HEARTBEAT.md`, or `BOOTSTRAP.md`). Codex-native subagents follow the same boundary: `TOOLS.md` stays in inherited Codex thread instructions, while parent-only persona, identity, and user files are injected as turn-scoped collaboration instructions so children do not clone them.
- Sub-agent context only injects `AGENTS.md` and `TOOLS.md` (no `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md`, or `BOOTSTRAP.md`). Codex-native subagents follow the same boundary: `TOOLS.md` stays in inherited Codex thread instructions, while parent-only persona, identity, and user files are injected as turn-scoped collaboration instructions so children do not clone them.
- Maximum nesting depth is 5 (`maxSpawnDepth` range: 1-5). Depth 2 is recommended for most use cases.
- `maxChildrenPerAgent` caps active children per session (default `5`, range `1-20`).
@@ -28,7 +28,7 @@ scenario:
config:
prompt: |-
Subagent fanout synthesis check: delegate exactly two bounded subagents sequentially using sessions_spawn, not ACP.
First spawn exactly one child with label qa-fanout-alpha and task: verify that `HEARTBEAT.md` exists and reply exactly `ok` if it does. Wait for that child to finish.
First spawn exactly one child with label qa-fanout-alpha and task: verify that `AGENTS.md` exists and reply exactly `ok` if it does. Wait for that child to finish.
Then spawn exactly one child with label qa-fanout-beta and task: verify that `repo/qa/scenarios/agents/subagent-fanout-synthesis.yaml` exists and reply exactly `ok` if it does. Wait for that child to finish.
Do not spawn any more children after qa-fanout-beta finishes.
Then reply with exactly these two lines and nothing else:
@@ -89,7 +89,7 @@ flow:
expr: "env.providerMode === 'mock-openai' ? config.expectedChildLabels[1] : `${config.expectedChildLabels[1]}-${attempt}`"
- set: prompt
value:
expr: "`Subagent fanout synthesis check: delegate exactly two bounded subagents sequentially using sessions_spawn, not ACP.\nFirst spawn exactly one child with label ${alphaLabel} and task: verify that \\`HEARTBEAT.md\\` exists and reply exactly \\`ok\\` if it does. Wait for that child to finish.\nThen spawn exactly one child with label ${betaLabel} and task: verify that \\`repo/qa/scenarios/agents/subagent-fanout-synthesis.yaml\\` exists and reply exactly \\`ok\\` if it does. Wait for that child to finish.\nDo not spawn any more children after ${betaLabel} finishes.\nThen reply with exactly these two lines and nothing else:\nsubagent-1: ok\nsubagent-2: ok`"
expr: "`Subagent fanout synthesis check: delegate exactly two bounded subagents sequentially using sessions_spawn, not ACP.\nFirst spawn exactly one child with label ${alphaLabel} and task: verify that \\`AGENTS.md\\` exists and reply exactly \\`ok\\` if it does. Wait for that child to finish.\nThen spawn exactly one child with label ${betaLabel} and task: verify that \\`repo/qa/scenarios/agents/subagent-fanout-synthesis.yaml\\` exists and reply exactly \\`ok\\` if it does. Wait for that child to finish.\nDo not spawn any more children after ${betaLabel} finishes.\nThen reply with exactly these two lines and nothing else:\nsubagent-1: ok\nsubagent-2: ok`"
- set: sessionKey
value:
expr: "`agent:qa:fanout:${attempt}:${randomUUID().slice(0, 8)}`"
-2
View File
@@ -17,7 +17,6 @@ import {
import {
DEFAULT_AGENTS_FILENAME,
DEFAULT_BOOTSTRAP_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_SOUL_FILENAME,
DEFAULT_TOOLS_FILENAME,
@@ -43,7 +42,6 @@ export async function ensureSandboxWorkspace(
DEFAULT_IDENTITY_FILENAME,
DEFAULT_USER_FILENAME,
DEFAULT_BOOTSTRAP_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
];
for (const name of files) {
const src = path.join(seed, name);
+1 -1
View File
@@ -84,7 +84,7 @@ async function readWorkspaceFileWithGuards(params: {
try {
// A transient FS race (EAGAIN/EWOULDBLOCK/EINTR under load) on the open or
// read must not drop the agent's bootstrap file for the turn — this reader
// runs every turn for AGENTS/SOUL/HEARTBEAT/etc. Retry the whole open+read so
// runs every turn for AGENTS/SOUL/TOOLS/etc. Retry the whole open+read so
// each attempt uses a fresh fd (retrying readFileSync on the same fd could
// return truncated content after a partial read); the inode-identity guard
// in openRootFile still protects against a swapped file between attempts.
+1 -1
View File
@@ -513,7 +513,7 @@ describe("Dockerfile", () => {
expect(workflow).toContain("Smoke test arm64 runtime workspace templates");
expect(workflow).toContain("test -f /app/src/agents/templates/HEARTBEAT.md");
expect(workflow).toContain('grep -F "Missing workspace template:"');
expect(workflow).toContain('test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"');
expect(workflow).not.toContain('test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"');
});
it("keeps only the runtime-assets prune proof in full release validation", async () => {
@@ -2935,6 +2935,27 @@ describe("agents.files.list", () => {
expect(names).toContain("BOOTSTRAP.md");
});
it("does not expose retired HEARTBEAT.md workspace files", async () => {
const names = await listAgentFileNames();
expect(names).not.toContain("HEARTBEAT.md");
});
it("rejects writes to retired HEARTBEAT.md workspace files", async () => {
const { respond, promise } = makeCall("agents.files.set", {
agentId: "main",
name: "HEARTBEAT.md",
content: "legacy checklist",
});
await promise;
expectRecordFields(expectRespondErrorContaining(respond, "unsupported file"), {
code: "INVALID_REQUEST",
message: 'unsupported file "HEARTBEAT.md"',
});
expect(mocks.fsMkdir).not.toHaveBeenCalled();
expect(mocks.rootWrite).not.toHaveBeenCalled();
});
it("hides BOOTSTRAP.md when workspace setup is complete", async () => {
mockWorkspaceStateRead({ setupCompletedAt: "2026-02-15T14:00:00.000Z" });
-2
View File
@@ -58,7 +58,6 @@ import {
import {
DEFAULT_AGENTS_FILENAME,
DEFAULT_BOOTSTRAP_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_MEMORY_FILENAME,
DEFAULT_SOUL_FILENAME,
@@ -110,7 +109,6 @@ const BOOTSTRAP_FILE_NAMES = [
DEFAULT_TOOLS_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_USER_FILENAME,
DEFAULT_HEARTBEAT_FILENAME,
DEFAULT_BOOTSTRAP_FILENAME,
] as const;
const BOOTSTRAP_FILE_NAMES_POST_ONBOARDING = BOOTSTRAP_FILE_NAMES.filter(
@@ -50,4 +50,4 @@ workspace root.
All paths are resolved from the workspace and must stay inside it (including realpath checks).
Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`,
`IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`).
`IDENTITY.md`, `USER.md`, `BOOTSTRAP.md`, `MEMORY.md`).
+5 -5
View File
@@ -480,8 +480,8 @@ describe("renderAgentFiles", () => {
missing: false,
},
{
name: "HEARTBEAT.md",
path: "/tmp/workspace/HEARTBEAT.md",
name: "SOUL.md",
path: "/tmp/workspace/SOUL.md",
missing: false,
},
],
@@ -501,9 +501,9 @@ describe("renderAgentFiles", () => {
container,
);
const heartbeatTab = expectAgentTab(container, "HEARTBEAT");
expect(heartbeatTab.disabled).toBe(true);
heartbeatTab.click();
const soulTab = expectAgentTab(container, "SOUL");
expect(soulTab.disabled).toBe(true);
soulTab.click();
expect(onSelectFile).not.toHaveBeenCalled();
});