mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 10:57:40 +00:00
fix(heartbeat): pin HEARTBEAT.md reads to workspace path
This commit is contained in:
@@ -1080,9 +1080,28 @@ describe("runHeartbeatOnce", () => {
|
||||
reason: params.reason,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
});
|
||||
return { res, replySpy, sendWhatsApp };
|
||||
return { res, replySpy, sendWhatsApp, workspaceDir };
|
||||
}
|
||||
|
||||
it("adds explicit workspace HEARTBEAT.md path guidance to heartbeat prompts", async () => {
|
||||
const { res, replySpy, sendWhatsApp, workspaceDir } = await runHeartbeatFileScenario({
|
||||
fileState: "actionable",
|
||||
reason: "interval",
|
||||
replyText: "Checked logs and PRs",
|
||||
});
|
||||
try {
|
||||
expect(res.status).toBe("ran");
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const calledCtx = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||
const expectedPath = path.join(workspaceDir, "HEARTBEAT.md").replace(/\\/g, "/");
|
||||
expect(calledCtx.Body).toContain(`use workspace file ${expectedPath} (exact case)`);
|
||||
expect(calledCtx.Body).toContain("Do not read docs/heartbeat.md.");
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
|
||||
@@ -560,11 +560,24 @@ type HeartbeatPromptResolution = {
|
||||
hasCronEvents: boolean;
|
||||
};
|
||||
|
||||
function appendHeartbeatWorkspacePathHint(prompt: string, workspaceDir: string): string {
|
||||
if (!/heartbeat\.md/i.test(prompt)) {
|
||||
return prompt;
|
||||
}
|
||||
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME).replace(/\\/g, "/");
|
||||
const hint = `When reading HEARTBEAT.md, use workspace file ${heartbeatFilePath} (exact case). Do not read docs/heartbeat.md.`;
|
||||
if (prompt.includes(hint)) {
|
||||
return prompt;
|
||||
}
|
||||
return `${prompt}\n${hint}`;
|
||||
}
|
||||
|
||||
function resolveHeartbeatRunPrompt(params: {
|
||||
cfg: OpenClawConfig;
|
||||
heartbeat?: HeartbeatConfig;
|
||||
preflight: HeartbeatPreflight;
|
||||
canRelayToUser: boolean;
|
||||
workspaceDir: string;
|
||||
}): HeartbeatPromptResolution {
|
||||
const pendingEventEntries = params.preflight.pendingEventEntries;
|
||||
const pendingEvents = params.preflight.shouldInspectPendingEvents
|
||||
@@ -579,11 +592,12 @@ function resolveHeartbeatRunPrompt(params: {
|
||||
.map((event) => event.text);
|
||||
const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
|
||||
const hasCronEvents = cronEvents.length > 0;
|
||||
const prompt = hasExecCompletion
|
||||
const basePrompt = hasExecCompletion
|
||||
? buildExecEventPrompt({ deliverToUser: params.canRelayToUser })
|
||||
: hasCronEvents
|
||||
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
||||
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
||||
const prompt = appendHeartbeatWorkspacePathHint(basePrompt, params.workspaceDir);
|
||||
|
||||
return { prompt, hasExecCompletion, hasCronEvents };
|
||||
}
|
||||
@@ -668,11 +682,13 @@ export async function runHeartbeatOnce(opts: {
|
||||
const canRelayToUser = Boolean(
|
||||
delivery.channel !== "none" && delivery.to && visibility.showAlerts,
|
||||
);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const { prompt, hasExecCompletion, hasCronEvents } = resolveHeartbeatRunPrompt({
|
||||
cfg,
|
||||
heartbeat,
|
||||
preflight,
|
||||
canRelayToUser,
|
||||
workspaceDir,
|
||||
});
|
||||
const ctx = {
|
||||
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
|
||||
|
||||
Reference in New Issue
Block a user