diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef474816..34d3c009f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Heartbeat: stop auto-creating `HEARTBEAT.md` during workspace bootstrap so missing files continue to run heartbeat as documented. (#11766) Thanks @shadril238. - CLI: lazily load outbound provider dependencies and remove forced success-path exits so commands terminate naturally without killing intentional long-running foreground actions. (#12906) Thanks @DrCrinkle. - Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner. - Discord: route autoThread replies to existing threads instead of the root channel. (#8302) Thanks @gavinbmoore, @thewilloftheshadow. diff --git a/src/agents/workspace.e2e.test.ts b/src/agents/workspace.e2e.test.ts index d4f842e6e..e34ff634a 100644 --- a/src/agents/workspace.e2e.test.ts +++ b/src/agents/workspace.e2e.test.ts @@ -1,9 +1,14 @@ +import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; import { + DEFAULT_AGENTS_FILENAME, + DEFAULT_BOOTSTRAP_FILENAME, + DEFAULT_HEARTBEAT_FILENAME, DEFAULT_MEMORY_ALT_FILENAME, DEFAULT_MEMORY_FILENAME, + ensureAgentWorkspace, loadWorkspaceBootstrapFiles, resolveDefaultAgentWorkspaceDir, } from "./workspace.js"; @@ -19,6 +24,21 @@ describe("resolveDefaultAgentWorkspaceDir", () => { }); }); +describe("ensureAgentWorkspace", () => { + it("does not create HEARTBEAT.md during bootstrap file initialization", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-init-"); + + const result = await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect(fs.access(path.join(tempDir, DEFAULT_AGENTS_FILENAME))).resolves.toBeUndefined(); + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + await expect(fs.access(path.join(tempDir, DEFAULT_HEARTBEAT_FILENAME))).rejects.toThrow(); + expect("heartbeatPath" in result).toBe(false); + }); +}); + describe("loadWorkspaceBootstrapFiles", () => { it("includes MEMORY.md when present", async () => { const tempDir = await makeTempWorkspace("openclaw-workspace-"); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index c13fe29f7..db074d943 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -173,7 +173,6 @@ export async function ensureAgentWorkspace(params?: { toolsPath?: string; identityPath?: string; userPath?: string; - heartbeatPath?: string; bootstrapPath?: string; }> { const rawDir = params?.dir?.trim() ? params.dir.trim() : DEFAULT_AGENT_WORKSPACE_DIR; @@ -189,11 +188,13 @@ export async function ensureAgentWorkspace(params?: { const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME); const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME); const userPath = path.join(dir, DEFAULT_USER_FILENAME); - const heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME); + // HEARTBEAT.md is intentionally NOT created from template. + // Per docs: "If the file is missing, the heartbeat still runs and the model decides what to do." + // Creating it from template (which is effectively empty) would cause heartbeat to be skipped. const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME); const isBrandNewWorkspace = await (async () => { - const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath, heartbeatPath]; + const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath]; const existing = await Promise.all( paths.map(async (p) => { try { @@ -212,7 +213,6 @@ export async function ensureAgentWorkspace(params?: { const toolsTemplate = await loadTemplate(DEFAULT_TOOLS_FILENAME); const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME); const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME); - const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME); const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME); await writeFileIfMissing(agentsPath, agentsTemplate); @@ -220,7 +220,6 @@ export async function ensureAgentWorkspace(params?: { await writeFileIfMissing(toolsPath, toolsTemplate); await writeFileIfMissing(identityPath, identityTemplate); await writeFileIfMissing(userPath, userTemplate); - await writeFileIfMissing(heartbeatPath, heartbeatTemplate); if (isBrandNewWorkspace) { await writeFileIfMissing(bootstrapPath, bootstrapTemplate); } @@ -233,7 +232,6 @@ export async function ensureAgentWorkspace(params?: { toolsPath, identityPath, userPath, - heartbeatPath, bootstrapPath, }; }