mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:30:42 +00:00
fix(heartbeat): respect custom heartbeat prompt
This commit is contained in:
@@ -1018,6 +1018,7 @@ describe("runHeartbeatOnce", () => {
|
|||||||
reason?: "interval" | "wake";
|
reason?: "interval" | "wake";
|
||||||
queueCronEvent?: boolean;
|
queueCronEvent?: boolean;
|
||||||
replyText?: string;
|
replyText?: string;
|
||||||
|
cfgOverrides?: Partial<OpenClawConfig>;
|
||||||
}) {
|
}) {
|
||||||
const tmpDir = await createCaseDir("openclaw-hb");
|
const tmpDir = await createCaseDir("openclaw-hb");
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
@@ -1041,7 +1042,7 @@ describe("runHeartbeatOnce", () => {
|
|||||||
await fs.mkdir(path.join(workspaceDir, "HEARTBEAT.md"), { recursive: true });
|
await fs.mkdir(path.join(workspaceDir, "HEARTBEAT.md"), { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfgBase: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
workspace: workspaceDir,
|
workspace: workspaceDir,
|
||||||
@@ -1051,6 +1052,9 @@ describe("runHeartbeatOnce", () => {
|
|||||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
};
|
};
|
||||||
|
const cfg: OpenClawConfig = params.cfgOverrides
|
||||||
|
? { ...cfgBase, ...params.cfgOverrides }
|
||||||
|
: cfgBase;
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
storePath,
|
storePath,
|
||||||
@@ -1083,7 +1087,7 @@ describe("runHeartbeatOnce", () => {
|
|||||||
return { res, replySpy, sendWhatsApp, workspaceDir };
|
return { res, replySpy, sendWhatsApp, workspaceDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
it("adds explicit workspace HEARTBEAT.md path guidance to heartbeat prompts", async () => {
|
it("adds explicit workspace HEARTBEAT.md path guidance to default heartbeat prompts", async () => {
|
||||||
const { res, replySpy, sendWhatsApp, workspaceDir } = await runHeartbeatFileScenario({
|
const { res, replySpy, sendWhatsApp, workspaceDir } = await runHeartbeatFileScenario({
|
||||||
fileState: "actionable",
|
fileState: "actionable",
|
||||||
reason: "interval",
|
reason: "interval",
|
||||||
@@ -1102,6 +1106,35 @@ describe("runHeartbeatOnce", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not mutate a custom heartbeat prompt", async () => {
|
||||||
|
const customPrompt =
|
||||||
|
"Read HEARTBEAT.md if it exists (workspace context). Use the system prompt only.";
|
||||||
|
const { res, replySpy } = await runHeartbeatFileScenario({
|
||||||
|
fileState: "actionable",
|
||||||
|
reason: "interval",
|
||||||
|
replyText: "Checked logs and PRs",
|
||||||
|
cfgOverrides: {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
prompt: customPrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
expect(res.status).toBe("ran");
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const calledCtx = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||||
|
expect(calledCtx.Body).toContain(customPrompt);
|
||||||
|
expect(calledCtx.Body).not.toContain("Do not read docs/heartbeat.md.");
|
||||||
|
expect(calledCtx.Body).not.toContain("use workspace file");
|
||||||
|
} finally {
|
||||||
|
replySpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
|
it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
|
||||||
const cases: Array<{
|
const cases: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -560,7 +560,15 @@ type HeartbeatPromptResolution = {
|
|||||||
hasCronEvents: boolean;
|
hasCronEvents: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function appendHeartbeatWorkspacePathHint(prompt: string, workspaceDir: string): string {
|
function appendHeartbeatWorkspacePathHint(params: {
|
||||||
|
prompt: string;
|
||||||
|
workspaceDir: string;
|
||||||
|
shouldAppend: boolean;
|
||||||
|
}): string {
|
||||||
|
const { prompt, workspaceDir, shouldAppend } = params;
|
||||||
|
if (!shouldAppend) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
if (!/heartbeat\.md/i.test(prompt)) {
|
if (!/heartbeat\.md/i.test(prompt)) {
|
||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
@@ -597,7 +605,16 @@ function resolveHeartbeatRunPrompt(params: {
|
|||||||
: hasCronEvents
|
: hasCronEvents
|
||||||
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
||||||
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
||||||
const prompt = appendHeartbeatWorkspacePathHint(basePrompt, params.workspaceDir);
|
const hasExplicitPrompt = Boolean(
|
||||||
|
params.heartbeat?.prompt?.trim() || params.cfg.agents?.defaults?.heartbeat?.prompt?.trim(),
|
||||||
|
);
|
||||||
|
const prompt = appendHeartbeatWorkspacePathHint({
|
||||||
|
prompt: basePrompt,
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
// Only append the hint when using the built-in default heartbeat prompt.
|
||||||
|
// If the user configured a custom prompt, do not mutate it.
|
||||||
|
shouldAppend: !hasExplicitPrompt && !hasExecCompletion && !hasCronEvents,
|
||||||
|
});
|
||||||
|
|
||||||
return { prompt, hasExecCompletion, hasCronEvents };
|
return { prompt, hasExecCompletion, hasCronEvents };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user