fix: Make cron systemEvents trigger autonomous agent action

Fixes #4107

Problem:
- Cron jobs with systemEvent payloads enqueue events as passive text
- Agent sees events but doesn't autonomously process them
- Expected behavior: agent should execute complex tasks (spawn subagents, etc.)

Root Cause:
- systemEvents are injected as 'System: [timestamp] text' messages
- Standard heartbeat prompt is passive: 'Read HEARTBEAT.md... reply HEARTBEAT_OK'
- No explicit instruction to process and act on systemEvents
- Exec completion events already had directive prompt, but generic events didn't

Solution:
- Added SYSTEM_EVENT_PROMPT constant with directive language
- Modified heartbeat runner to detect generic (non-exec) systemEvents
- When generic events are present, use directive prompt instead of passive one
- Directive prompt explicitly instructs agent to:
  1. Check HEARTBEAT.md for event-specific handling
  2. Execute required actions (spawn subagent, perform task, etc.)
  3. Reply HEARTBEAT_OK only if no action needed

Changes:
- src/infra/heartbeat-runner.ts:
  - Added SYSTEM_EVENT_PROMPT constant (lines 100-105)
  - Modified prompt selection logic to detect generic systemEvents (lines 510-523)
  - Updated Provider field to reflect 'system-event' context

Testing:
- Build passes (TypeScript compilation successful)
- Pattern follows existing exec-event precedent
- Backward compatible (only affects sessions with pending systemEvents)

Follow-up:
- Add integration test for cron → subagent spawn workflow
- Update documentation on systemEvent best practices
This commit is contained in:
spiceoogway 2026-01-30 02:48:16 -05:00
parent a39811caca
commit cc2f8e3351

View File

@ -97,6 +97,14 @@ const EXEC_EVENT_PROMPT =
"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.";
// This prompt is used when generic system events (e.g., from cron jobs) are pending.
// It explicitly instructs the agent to process the events rather than just acknowledging them.
const SYSTEM_EVENT_PROMPT =
"You have received one or more system events (shown in the system messages above). " +
"Read HEARTBEAT.md if it exists for instructions on how to handle specific event types. " +
"If an event requires action (e.g., spawning a subagent, performing a task), execute that action now. " +
"If no action is needed, reply HEARTBEAT_OK.";
function resolveActiveHoursTimezone(cfg: OpenClawConfig, raw?: string): string {
const trimmed = raw?.trim();
if (!trimmed || trimmed === "user") {
@ -499,15 +507,26 @@ export async function runHeartbeatOnce(opts: {
// If so, use a specialized prompt that instructs the model to relay the result
// instead of the standard heartbeat prompt with "reply HEARTBEAT_OK".
const isExecEvent = opts.reason === "exec-event";
const pendingEvents = isExecEvent ? peekSystemEvents(sessionKey) : [];
const pendingEvents = peekSystemEvents(sessionKey);
const hasExecCompletion = pendingEvents.some((evt) => evt.includes("Exec finished"));
// Check for generic (non-exec) system events that may require action (e.g., from cron jobs).
// Use a directive prompt to ensure the agent processes them rather than just acknowledging.
const hasGenericSystemEvents = pendingEvents.length > 0 && !hasExecCompletion;
const prompt = hasExecCompletion ? EXEC_EVENT_PROMPT : resolveHeartbeatPrompt(cfg, heartbeat);
const prompt = hasExecCompletion
? EXEC_EVENT_PROMPT
: hasGenericSystemEvents
? SYSTEM_EVENT_PROMPT
: resolveHeartbeatPrompt(cfg, heartbeat);
const ctx = {
Body: prompt,
From: sender,
To: sender,
Provider: hasExecCompletion ? "exec-event" : "heartbeat",
Provider: hasExecCompletion
? "exec-event"
: hasGenericSystemEvents
? "system-event"
: "heartbeat",
SessionKey: sessionKey,
};
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {