diff --git a/src/infra/heartbeat-runner.ghost-reminder.test.ts b/src/infra/heartbeat-runner.ghost-reminder.test.ts index 029ab8af694..6d553ad302e 100644 --- a/src/infra/heartbeat-runner.ghost-reminder.test.ts +++ b/src/infra/heartbeat-runner.ghost-reminder.test.ts @@ -347,7 +347,7 @@ describe("Ghost reminder bug (issue #13317)", () => { reason: "exec-event", target: "none", enqueue: (sessionKey) => { - enqueueSystemEvent("exec finished: deploy succeeded", { sessionKey }); + enqueueSystemEvent("exec finished: deploy succeeded", { sessionKey, trusted: false }); }, }); @@ -358,6 +358,23 @@ describe("Ghost reminder bug (issue #13317)", () => { expect(sendTelegram).not.toHaveBeenCalled(); }); + it("includes untrusted exec completion details in user-relay prompts", async () => { + const { result, sendTelegram, calledCtx } = await runHeartbeatCase({ + tmpPrefix: "openclaw-exec-untrusted-relay-", + replyText: "Deploy succeeded", + reason: "exec-event", + enqueue: (sessionKey) => { + enqueueSystemEvent("exec finished: deploy succeeded", { sessionKey, trusted: false }); + }, + }); + + expect(result.status).toBe("ran"); + expect(calledCtx?.Provider).toBe("exec-event"); + expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(true); + expect(calledCtx?.Body).toContain("exec finished: deploy succeeded"); + expect(sendTelegram).toHaveBeenCalled(); + }); + it("classifies hook:wake exec completions as exec-event prompts", async () => { const { result, sendTelegram, calledCtx } = await runHeartbeatCase({ tmpPrefix: "openclaw-hook-exec-", diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 74350d4abdb..6992f8bb044 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -676,13 +676,12 @@ function resolveHeartbeatRunPrompt(params: { .map((event) => event.text); const execEvents = params.preflight.shouldInspectPendingEvents ? pendingEventEntries - .filter((event) => event.trusted !== false && isExecCompletionEvent(event.text)) + .filter((event) => 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 if (params.preflight.tasks && params.preflight.tasks.length > 0) { const tasks = params.preflight.tasks; const dueTasks = tasks.filter((task) => @@ -701,7 +700,6 @@ ${taskList} After completing all due tasks, reply HEARTBEAT_OK.`; - // Preserve HEARTBEAT.md directives (non-task content) if (params.heartbeatFileContent) { const directives = params.heartbeatFileContent .replace(/^[\s\S]*?^tasks:[\s\S]*?(?=^[^\s]|^$)/m, "") @@ -712,11 +710,9 @@ After completing all due tasks, reply HEARTBEAT_OK.`; } return { prompt, hasExecCompletion: false, hasCronEvents: false }; } - // No tasks due - skip this heartbeat to avoid wasteful API calls return { prompt: null, hasExecCompletion: false, hasCronEvents: false }; } - // Fallback to original behavior const basePrompt = hasExecCompletion ? buildExecEventPrompt(execEvents, { deliverToUser: params.canRelayToUser }) : hasCronEvents