Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions api/src/services/cronJobsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ function isUuid(str) {
return typeof str === 'string' && UUID_RE.test(str);
}

function resolveJobEntry(jobs, identifier) {
if (!jobs || !identifier) return null;
if (jobs[identifier]) return { key: identifier, job: jobs[identifier] };

for (const [key, job] of Object.entries(jobs)) {
if (job?.jobId === identifier || job?.id === identifier) {
return { key, job };
}
}

return null;
}

/**
* Compute the next run timestamp (ms) for a cron job based on its schedule.
* Returns null if the schedule cannot be parsed.
Expand Down Expand Up @@ -940,17 +953,41 @@ async function triggerCronJob(jobId) {
});
}

// Try Gateway cron.run via WebSocket RPC before file fallback
try {
const { gatewayWsRpc } = require('./openclawGatewayClient');
const result = await gatewayWsRpc('cron.run', { jobId });
if (result) {
logger.info('Cron job triggered via Gateway WS RPC', { jobId });
const job = result.job || result;
return fromOfficialFormat({
...job,
jobId: job.jobId || jobId,
source: 'gateway',
});
}
} catch (wsErr) {
if (wsErr.code === 'SERVICE_NOT_CONFIGURED' || wsErr.code === 'SERVICE_UNAVAILABLE') {
throw wsErr;
}
logger.warn('Gateway cron.run WS RPC failed, falling back to file trigger', {
error: wsErr.message,
code: wsErr.code,
});
}

// Fallback: set nextRunAtMs to near-immediate so the Gateway fires it
const jobs = await readCronJobs();

if (!jobs[jobId]) {
const resolved = resolveJobEntry(jobs, jobId);
if (!resolved) {
const err = new Error(`Cron job not found: ${jobId}`);
err.status = 404;
err.code = 'NOT_FOUND';
throw err;
}

const job = jobs[jobId];
const { key: mapKey, job } = resolved;

if (job.enabled === false) {
const err = new Error('Cannot trigger a disabled cron job. Enable it first.');
Expand All @@ -962,7 +999,7 @@ async function triggerCronJob(jobId) {
// Set nextRunAtMs to 3 seconds from now — the Gateway picks it up on
// its next 60 s timer tick and fires the job.
job.state = { ...(job.state || {}), nextRunAtMs: Date.now() + 3000 };
jobs[jobId] = job;
jobs[mapKey] = job;
await writeCronJobs(jobs);

logger.info('Cron job trigger requested via file fallback (nextRunAtMs set to now)', {
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/CronJobList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function formatModel(model) {
return modelPart;
}

function getStatusBadge(status, enabled, nextRunAt, lastRunAt) {
function getStatusBadge(status, enabled, nextRunAt, lastRunAt, isHeartbeat = false) {
if (enabled === false) {
return {
classes: 'bg-dark-600/10 text-dark-400 border-dark-600/20',
Expand All @@ -114,7 +114,7 @@ function getStatusBadge(status, enabled, nextRunAt, lastRunAt) {
}

// Check if enabled but has never been scheduled (no next or last run)
if (enabled !== false && !nextRunAt && !lastRunAt) {
if (enabled !== false && !nextRunAt && !lastRunAt && !isHeartbeat) {
return {
classes: 'bg-yellow-600/10 text-yellow-500 border-yellow-500/20',
icon: ExclamationTriangleIcon,
Expand Down Expand Up @@ -151,9 +151,9 @@ function getStatusBadge(status, enabled, nextRunAt, lastRunAt) {
}

function CronJobRow({ job }) {
const badge = getStatusBadge(job.status, job.enabled, job.nextRunAt, job.lastRunAt);
const BadgeIcon = badge.icon;
const isHeartbeat = job.source === 'config' || job.payload?.kind === 'heartbeat';
const badge = getStatusBadge(job.status, job.enabled, job.nextRunAt, job.lastRunAt, isHeartbeat);
const BadgeIcon = badge.icon;

// Extract prompt from payload
const prompt = job.payload?.message || job.payload?.text || job.prompt || null;
Expand Down
15 changes: 12 additions & 3 deletions web/src/pages/CronJobs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function getStatusBadge(
lastRunAtMs,
cronExpr = null,
cronTz = null,
isHeartbeat = false,
) {
if (enabled === false) {
return {
Expand All @@ -230,7 +231,7 @@ function getStatusBadge(
label: 'disabled',
};
}
if (enabled !== false && !nextRunAtMs && !lastRunAtMs) {
if (enabled !== false && !nextRunAtMs && !lastRunAtMs && !isHeartbeat) {
return {
classes: 'bg-yellow-600/10 text-yellow-500 border-yellow-500/20',
icon: ExclamationTriangleIcon,
Expand Down Expand Up @@ -297,9 +298,17 @@ function CronJobRow({
const cronExpr = schedule.kind === 'cron' ? schedule.expr : null;
const cronTz = schedule.tz || null;

const badge = getStatusBadge(lastStatus, job.enabled, nextRunAtMs, lastRunAtMs, cronExpr, cronTz);
const BadgeIcon = badge.icon;
const isHeartbeat = job.source === 'config' || job.payload?.kind === 'heartbeat';
const badge = getStatusBadge(
lastStatus,
job.enabled,
nextRunAtMs,
lastRunAtMs,
cronExpr,
cronTz,
isHeartbeat,
);
const BadgeIcon = badge.icon;

const prompt =
job.payload?.message || job.payload?.text || job.payload?.prompt || job.prompt || null;
Expand Down
Loading