diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5ea9626aa..2563f854b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,6 @@ Docs: https://docs.openclaw.ai - Heartbeat: prevent scheduler silent-death races during runner reloads, preserve retry cooldown backoff under wake bursts, and prioritize user/action wake causes over interval/retry reasons when coalescing. (#15108) Thanks @joeykrug. - Heartbeat: allow explicit wake (`wake`) and hook wake (`hook:*`) reasons to run even when `HEARTBEAT.md` is effectively empty so queued system events are processed. (#14527) Thanks @arosstale. - Auto-reply/Heartbeat: strip sentence-ending `HEARTBEAT_OK` tokens even when followed by up to 4 punctuation characters, while preserving surrounding sentence punctuation. (#15847) Thanks @Spacefish. -- Agents/Heartbeat: stop auto-creating `HEARTBEAT.md` during workspace bootstrap so missing files continue to run heartbeat as documented. (#11766) Thanks @shadril238. - Sessions/Agents: pass `agentId` when resolving existing transcript paths in reply runs so non-default agents and heartbeat/chat handlers no longer fail with `Session file path must be within sessions directory`. (#15141) Thanks @Goldenmonstew. - Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman. - Sessions: archive previous transcript files on `/new` and `/reset` session resets (including gateway `sessions.reset`) so stale transcripts do not accumulate on disk. (#14869) Thanks @mcaxtr. diff --git a/src/agents/workspace.e2e.test.ts b/src/agents/workspace.e2e.test.ts index e34ff634a86..d4f842e6ea0 100644 --- a/src/agents/workspace.e2e.test.ts +++ b/src/agents/workspace.e2e.test.ts @@ -1,14 +1,9 @@ -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"; @@ -24,21 +19,6 @@ 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 db074d94303..c13fe29f72a 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -173,6 +173,7 @@ 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; @@ -188,13 +189,11 @@ 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); - // 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 heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME); const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME); const isBrandNewWorkspace = await (async () => { - const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath]; + const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath, heartbeatPath]; const existing = await Promise.all( paths.map(async (p) => { try { @@ -213,6 +212,7 @@ 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,6 +220,7 @@ export async function ensureAgentWorkspace(params?: { await writeFileIfMissing(toolsPath, toolsTemplate); await writeFileIfMissing(identityPath, identityTemplate); await writeFileIfMissing(userPath, userTemplate); + await writeFileIfMissing(heartbeatPath, heartbeatTemplate); if (isBrandNewWorkspace) { await writeFileIfMissing(bootstrapPath, bootstrapTemplate); } @@ -232,6 +233,7 @@ export async function ensureAgentWorkspace(params?: { toolsPath, identityPath, userPath, + heartbeatPath, bootstrapPath, }; }