mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
fix(heartbeat): include exec completion payloads
This commit is contained in:
@@ -47,18 +47,36 @@ describe("heartbeat event prompts", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "builds user-relay exec prompt by default",
|
||||
events: ["Exec finished (node=abc id=123, code 0)\nUploaded file"],
|
||||
opts: undefined,
|
||||
expected: ["Please relay the command output to the user", "If it failed"],
|
||||
unexpected: ["Handle the result internally"],
|
||||
expected: [
|
||||
"Exec finished",
|
||||
"Uploaded file",
|
||||
"Please relay the command output to the user",
|
||||
"If it failed",
|
||||
],
|
||||
unexpected: ["system messages above", "Handle the result internally"],
|
||||
},
|
||||
{
|
||||
name: "builds internal-only exec prompt when delivery is disabled",
|
||||
events: ["Exec failed (node=abc id=123, code 1)\nUpload failed"],
|
||||
opts: { deliverToUser: false },
|
||||
expected: ["Handle the result internally"],
|
||||
unexpected: ["Please relay the command output to the user"],
|
||||
expected: ["user delivery is disabled", "Handle the result internally", "HEARTBEAT_OK only"],
|
||||
unexpected: [
|
||||
"Upload failed",
|
||||
"system messages above",
|
||||
"Please relay the command output to the user",
|
||||
],
|
||||
},
|
||||
])("$name", ({ opts, expected, unexpected }) => {
|
||||
const prompt = buildExecEventPrompt(opts);
|
||||
{
|
||||
name: "suppresses empty exec completion prompts",
|
||||
events: ["", " "],
|
||||
opts: undefined,
|
||||
expected: ["no command output was found", "Reply HEARTBEAT_OK only"],
|
||||
unexpected: ["Please relay the command output to the user", "system messages above"],
|
||||
},
|
||||
])("$name", ({ events, opts, expected, unexpected }) => {
|
||||
const prompt = buildExecEventPrompt(events, opts);
|
||||
for (const part of expected) {
|
||||
expect(prompt).toContain(part);
|
||||
}
|
||||
@@ -66,6 +84,13 @@ describe("heartbeat event prompts", () => {
|
||||
expect(prompt).not.toContain(part);
|
||||
}
|
||||
});
|
||||
|
||||
it("truncates oversized user-relay exec prompt output", () => {
|
||||
const prompt = buildExecEventPrompt([`Exec finished: ${"x".repeat(8_100)}`]);
|
||||
|
||||
expect(prompt).toContain("[truncated]");
|
||||
expect(prompt.length).toBeLessThan(8_500);
|
||||
});
|
||||
});
|
||||
|
||||
describe("heartbeat event classification", () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
|
||||
const MAX_EXEC_EVENT_PROMPT_CHARS = 8_000;
|
||||
|
||||
// Build a dynamic prompt for cron events by embedding the actual event content.
|
||||
// This ensures the model sees the reminder text directly instead of relying on
|
||||
// "shown in the system messages above" which may not be visible in context.
|
||||
@@ -38,16 +40,32 @@ export function buildCronEventPrompt(
|
||||
);
|
||||
}
|
||||
|
||||
export function buildExecEventPrompt(opts?: { deliverToUser?: boolean }): string {
|
||||
export function buildExecEventPrompt(
|
||||
pendingEvents: string[],
|
||||
opts?: { deliverToUser?: boolean },
|
||||
): string {
|
||||
const deliverToUser = opts?.deliverToUser ?? true;
|
||||
const rawEventText = pendingEvents.join("\n").trim();
|
||||
const eventText =
|
||||
rawEventText.length > MAX_EXEC_EVENT_PROMPT_CHARS
|
||||
? `${rawEventText.slice(0, MAX_EXEC_EVENT_PROMPT_CHARS)}\n\n[truncated]`
|
||||
: rawEventText;
|
||||
if (!eventText) {
|
||||
return (
|
||||
"An async command completion event was triggered, but no command output was found. " +
|
||||
"Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run."
|
||||
);
|
||||
}
|
||||
if (!deliverToUser) {
|
||||
return (
|
||||
"An async command you ran earlier has completed. The result is shown in the system messages above. " +
|
||||
"Handle the result internally. Do not relay it to the user unless explicitly requested."
|
||||
"An async command completion event was triggered, but user delivery is disabled for this run. " +
|
||||
"Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output."
|
||||
);
|
||||
}
|
||||
return (
|
||||
"An async command you ran earlier has completed. The result is shown in the system messages above. " +
|
||||
"An async command you ran earlier has completed. The command completion details are:\n\n" +
|
||||
eventText +
|
||||
"\n\n" +
|
||||
"Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. " +
|
||||
"If it failed, explain what went wrong."
|
||||
);
|
||||
|
||||
@@ -667,9 +667,6 @@ function resolveHeartbeatRunPrompt(params: {
|
||||
heartbeatFileContent?: string;
|
||||
}): HeartbeatPromptResolution {
|
||||
const pendingEventEntries = params.preflight.pendingEventEntries;
|
||||
const pendingEvents = params.preflight.shouldInspectPendingEvents
|
||||
? pendingEventEntries.map((event) => event.text)
|
||||
: [];
|
||||
const cronEvents = pendingEventEntries
|
||||
.filter(
|
||||
(event) =>
|
||||
@@ -677,7 +674,12 @@ function resolveHeartbeatRunPrompt(params: {
|
||||
isCronSystemEvent(event.text),
|
||||
)
|
||||
.map((event) => event.text);
|
||||
const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
|
||||
const execEvents = params.preflight.shouldInspectPendingEvents
|
||||
? pendingEventEntries
|
||||
.filter((event) => event.trusted !== false && isExecCompletionEvent(event.text))
|
||||
.map((event) => event.text)
|
||||
: [];
|
||||
const hasExecCompletion = execEvents.length > 0;
|
||||
const hasCronEvents = cronEvents.length > 0;
|
||||
|
||||
// If tasks are defined, build a batched prompt with due tasks
|
||||
@@ -716,7 +718,7 @@ After completing all due tasks, reply HEARTBEAT_OK.`;
|
||||
|
||||
// Fallback to original behavior
|
||||
const basePrompt = hasExecCompletion
|
||||
? buildExecEventPrompt({ deliverToUser: params.canRelayToUser })
|
||||
? buildExecEventPrompt(execEvents, { deliverToUser: params.canRelayToUser })
|
||||
: hasCronEvents
|
||||
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
||||
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
||||
|
||||
Reference in New Issue
Block a user