mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 07:41:08 +00:00
fix(heartbeat): address review comments - parsing, timing, state, skips
- Fix YAML parsing to capture interval:/prompt: before breaking - Record task timestamps AFTER successful execution (not before) - Initialize task state on first run (handle undefined session) - Skip API call when no tasks due (return null) - Use startedAt consistently for due-task filtering Fixes: #3030568439, #3033833124, #3030570872, #3030568408, #3030570872, #3035434022, #3035434368
This commit is contained in:
committed by
Peter Steinberger
parent
728d14e918
commit
ba09426707
@@ -234,12 +234,8 @@ export function parseHeartbeatTasks(content: string): HeartbeatTask[] {
|
||||
if (nextTrimmed.startsWith("- name:")) {
|
||||
break;
|
||||
}
|
||||
// End of tasks block
|
||||
if (!nextTrimmed.startsWith(" ") && !nextTrimmed.startsWith("\t") && nextTrimmed) {
|
||||
inTasksBlock = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for task fields BEFORE checking for end of block
|
||||
if (nextTrimmed.startsWith("interval:")) {
|
||||
interval = nextTrimmed
|
||||
.replace("interval:", "")
|
||||
@@ -250,6 +246,10 @@ export function parseHeartbeatTasks(content: string): HeartbeatTask[] {
|
||||
.replace("prompt:", "")
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, "");
|
||||
} else if (!nextTrimmed.startsWith(" ") && !nextTrimmed.startsWith("\t") && nextTrimmed) {
|
||||
// End of tasks block
|
||||
inTasksBlock = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -537,12 +537,11 @@ function resolveHeartbeatRunPrompt(params: {
|
||||
// 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 nowMs = Date.now();
|
||||
const dueTasks = tasks.filter((task) =>
|
||||
isTaskDue(
|
||||
(params.preflight.session.entry?.heartbeatTaskState as Record<string, number>)?.[task.name],
|
||||
task.interval,
|
||||
nowMs,
|
||||
startedAt,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -555,9 +554,8 @@ ${taskList}
|
||||
After completing all due tasks, reply HEARTBEAT_OK.`;
|
||||
return { prompt, hasExecCompletion: false, hasCronEvents: false };
|
||||
}
|
||||
// No tasks due - still run but with empty task list
|
||||
const prompt = `No periodic tasks are due right now. Reply HEARTBEAT_OK.`;
|
||||
return { prompt, hasExecCompletion: false, hasCronEvents: false };
|
||||
// No tasks due - skip this heartbeat to avoid wasteful API calls
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback to original behavior
|
||||
@@ -698,25 +696,35 @@ export async function runHeartbeatOnce(opts: {
|
||||
workspaceDir,
|
||||
});
|
||||
|
||||
// Update task last run times BEFORE model runs - ensures timestamps are persisted
|
||||
// even when model completes with HEARTBEAT_OK (which triggers early return)
|
||||
if (preflight.tasks && preflight.tasks.length > 0) {
|
||||
// Track if heartbeat completed successfully (for updating task timestamps)
|
||||
let heartbeatSuccess = false;
|
||||
|
||||
// Update task last run times AFTER successful heartbeat completion
|
||||
const updateTaskTimestamps = async () => {
|
||||
if (!preflight.tasks || preflight.tasks.length === 0) return;
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
const current = store[sessionKey];
|
||||
if (current) {
|
||||
const taskState = (current.heartbeatTaskState as Record<string, number>) || {};
|
||||
for (const task of preflight.tasks) {
|
||||
if (isTaskDue(taskState[task.name], task.interval, startedAt)) {
|
||||
taskState[task.name] = startedAt;
|
||||
}
|
||||
// Initialize stub entry on first run when current doesn't exist
|
||||
const base = current ?? {
|
||||
sessionId: sessionKey,
|
||||
updatedAt: startedAt,
|
||||
createdAt: startedAt,
|
||||
messageCount: 0,
|
||||
lastMessageAt: startedAt,
|
||||
heartbeatTaskState: {}
|
||||
};
|
||||
const taskState = { ...((base.heartbeatTaskState as Record<string, number>) || {}) };
|
||||
|
||||
for (const task of preflight.tasks) {
|
||||
if (isTaskDue(taskState[task.name], task.interval, startedAt)) {
|
||||
taskState[task.name] = startedAt;
|
||||
}
|
||||
store[sessionKey] = {
|
||||
...current,
|
||||
heartbeatTaskState: taskState,
|
||||
};
|
||||
await saveSessionStore(storePath, store);
|
||||
}
|
||||
}
|
||||
|
||||
store[sessionKey] = { ...base, heartbeatTaskState: taskState };
|
||||
await saveSessionStore(storePath, store);
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
|
||||
@@ -1011,6 +1019,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
accountId: delivery.accountId,
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
||||
});
|
||||
heartbeatSuccess = true;
|
||||
await updateTaskTimestamps();
|
||||
return { status: "ran", durationMs: Date.now() - startedAt };
|
||||
} catch (err) {
|
||||
const reason = formatErrorMessage(err);
|
||||
|
||||
Reference in New Issue
Block a user