@elizaos/plugin-cron 2.0.0-alpha.6 → 2.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-IZYSN6UN.js → chunk-ZY3PG5VX.js} +1099 -1056
- package/dist/{dist-KM2GC6Y3.js → dist-MAGERK67.js} +1 -1
- package/dist/index-CcftVpZH.d.ts +553 -0
- package/dist/index.d.ts +561 -0
- package/dist/index.js +1905 -1810
- package/dist/otto/index.d.ts +2 -0
- package/dist/otto/index.js +1 -1
- package/package.json +47 -47
package/dist/index.js
CHANGED
|
@@ -17,13 +17,9 @@ import {
|
|
|
17
17
|
resolveHeartbeatConfig,
|
|
18
18
|
startHeartbeat,
|
|
19
19
|
wakeHeartbeatNow
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-ZY3PG5VX.js";
|
|
21
21
|
import "./chunk-MLKGABMK.js";
|
|
22
22
|
|
|
23
|
-
// src/services/cron-service.ts
|
|
24
|
-
import { Service, logger } from "@elizaos/core";
|
|
25
|
-
import { v4 as uuidv43 } from "uuid";
|
|
26
|
-
|
|
27
23
|
// src/constants.ts
|
|
28
24
|
var CRON_SERVICE_TYPE = "CRON";
|
|
29
25
|
var CRON_JOB_COMPONENT_PREFIX = "cron_job";
|
|
@@ -43,233 +39,6 @@ var CronActions = {
|
|
|
43
39
|
RUN_CRON: "RUN_CRON"
|
|
44
40
|
};
|
|
45
41
|
|
|
46
|
-
// src/storage/cron-storage.ts
|
|
47
|
-
import { v4 as uuidv4 } from "uuid";
|
|
48
|
-
var AsyncMutex = class {
|
|
49
|
-
queue = [];
|
|
50
|
-
locked = false;
|
|
51
|
-
async acquire() {
|
|
52
|
-
return new Promise((resolve) => {
|
|
53
|
-
const tryAcquire = () => {
|
|
54
|
-
if (!this.locked) {
|
|
55
|
-
this.locked = true;
|
|
56
|
-
resolve(() => this.release());
|
|
57
|
-
} else {
|
|
58
|
-
this.queue.push(tryAcquire);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
tryAcquire();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
release() {
|
|
65
|
-
this.locked = false;
|
|
66
|
-
const next = this.queue.shift();
|
|
67
|
-
if (next) {
|
|
68
|
-
next();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
var indexMutexes = /* @__PURE__ */ new Map();
|
|
73
|
-
function getIndexMutex(agentId) {
|
|
74
|
-
let mutex = indexMutexes.get(agentId);
|
|
75
|
-
if (!mutex) {
|
|
76
|
-
mutex = new AsyncMutex();
|
|
77
|
-
indexMutexes.set(agentId, mutex);
|
|
78
|
-
}
|
|
79
|
-
return mutex;
|
|
80
|
-
}
|
|
81
|
-
function getJobComponentType(jobId) {
|
|
82
|
-
return `${CRON_JOB_COMPONENT_PREFIX}:${jobId}`;
|
|
83
|
-
}
|
|
84
|
-
async function getJobIndex(runtime) {
|
|
85
|
-
const component = await runtime.getComponent(runtime.agentId, CRON_JOB_INDEX_COMPONENT);
|
|
86
|
-
if (!component) {
|
|
87
|
-
return { jobIds: [] };
|
|
88
|
-
}
|
|
89
|
-
return component.data;
|
|
90
|
-
}
|
|
91
|
-
async function saveJobIndex(runtime, index) {
|
|
92
|
-
const existing = await runtime.getComponent(runtime.agentId, CRON_JOB_INDEX_COMPONENT);
|
|
93
|
-
const component = {
|
|
94
|
-
id: existing?.id || uuidv4(),
|
|
95
|
-
entityId: runtime.agentId,
|
|
96
|
-
agentId: runtime.agentId,
|
|
97
|
-
roomId: runtime.agentId,
|
|
98
|
-
// Use agentId as room for agent-scoped data
|
|
99
|
-
worldId: existing?.worldId || uuidv4(),
|
|
100
|
-
sourceEntityId: runtime.agentId,
|
|
101
|
-
type: CRON_JOB_INDEX_COMPONENT,
|
|
102
|
-
createdAt: existing?.createdAt || Date.now(),
|
|
103
|
-
data: index
|
|
104
|
-
};
|
|
105
|
-
if (existing) {
|
|
106
|
-
await runtime.updateComponent(component);
|
|
107
|
-
} else {
|
|
108
|
-
await runtime.createComponent(component);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async function addToIndex(runtime, jobId) {
|
|
112
|
-
const mutex = getIndexMutex(runtime.agentId);
|
|
113
|
-
const release = await mutex.acquire();
|
|
114
|
-
try {
|
|
115
|
-
const index = await getJobIndex(runtime);
|
|
116
|
-
if (!index.jobIds.includes(jobId)) {
|
|
117
|
-
index.jobIds.push(jobId);
|
|
118
|
-
await saveJobIndex(runtime, index);
|
|
119
|
-
}
|
|
120
|
-
} finally {
|
|
121
|
-
release();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async function removeFromIndex(runtime, jobId) {
|
|
125
|
-
const mutex = getIndexMutex(runtime.agentId);
|
|
126
|
-
const release = await mutex.acquire();
|
|
127
|
-
try {
|
|
128
|
-
const index = await getJobIndex(runtime);
|
|
129
|
-
const filtered = index.jobIds.filter((id) => id !== jobId);
|
|
130
|
-
if (filtered.length !== index.jobIds.length) {
|
|
131
|
-
index.jobIds = filtered;
|
|
132
|
-
await saveJobIndex(runtime, index);
|
|
133
|
-
}
|
|
134
|
-
} finally {
|
|
135
|
-
release();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function validateJobData(data) {
|
|
139
|
-
if (!data || typeof data !== "object") {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const obj = data;
|
|
143
|
-
if (typeof obj.id !== "string" || !obj.id) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
if (typeof obj.name !== "string" || !obj.name) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
if (typeof obj.createdAtMs !== "number") {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
if (typeof obj.updatedAtMs !== "number") {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
if (!obj.schedule || typeof obj.schedule !== "object") {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
const schedule = obj.schedule;
|
|
159
|
-
const validScheduleKinds = ["at", "every", "cron"];
|
|
160
|
-
if (!validScheduleKinds.includes(schedule.kind)) {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
if (!obj.payload || typeof obj.payload !== "object") {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const payload = obj.payload;
|
|
167
|
-
const validPayloadKinds = ["prompt", "action", "event"];
|
|
168
|
-
if (!validPayloadKinds.includes(payload.kind)) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
if (!obj.state || typeof obj.state !== "object") {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
const state = obj.state;
|
|
175
|
-
if (typeof state.runCount !== "number" || typeof state.errorCount !== "number") {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const job = {
|
|
179
|
-
...data,
|
|
180
|
-
enabled: typeof obj.enabled === "boolean" ? obj.enabled : true,
|
|
181
|
-
deleteAfterRun: typeof obj.deleteAfterRun === "boolean" ? obj.deleteAfterRun : false
|
|
182
|
-
};
|
|
183
|
-
return job;
|
|
184
|
-
}
|
|
185
|
-
function matchesFilter(job, filter) {
|
|
186
|
-
if (!filter) return true;
|
|
187
|
-
if (filter.enabled !== void 0) {
|
|
188
|
-
if (job.enabled !== filter.enabled) return false;
|
|
189
|
-
} else if (!filter.includeDisabled && !job.enabled) {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
if (filter.tags?.length) {
|
|
193
|
-
if (!job.tags?.some((tag) => filter.tags.includes(tag))) return false;
|
|
194
|
-
}
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
function getCronStorage(runtime) {
|
|
198
|
-
return {
|
|
199
|
-
async getJob(jobId) {
|
|
200
|
-
const componentType = getJobComponentType(jobId);
|
|
201
|
-
const component = await runtime.getComponent(runtime.agentId, componentType);
|
|
202
|
-
if (!component) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
const validatedJob = validateJobData(component.data);
|
|
206
|
-
if (!validatedJob) {
|
|
207
|
-
console.warn(`[cron-storage] Invalid job data for ${jobId}, skipping`);
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
return validatedJob;
|
|
211
|
-
},
|
|
212
|
-
async saveJob(job) {
|
|
213
|
-
const componentType = getJobComponentType(job.id);
|
|
214
|
-
const existing = await runtime.getComponent(runtime.agentId, componentType);
|
|
215
|
-
const component = {
|
|
216
|
-
id: existing?.id || uuidv4(),
|
|
217
|
-
entityId: runtime.agentId,
|
|
218
|
-
agentId: runtime.agentId,
|
|
219
|
-
roomId: runtime.agentId,
|
|
220
|
-
worldId: existing?.worldId || uuidv4(),
|
|
221
|
-
sourceEntityId: runtime.agentId,
|
|
222
|
-
type: componentType,
|
|
223
|
-
createdAt: existing?.createdAt || job.createdAtMs,
|
|
224
|
-
data: job
|
|
225
|
-
};
|
|
226
|
-
if (existing) {
|
|
227
|
-
await runtime.updateComponent(component);
|
|
228
|
-
} else {
|
|
229
|
-
await runtime.createComponent(component);
|
|
230
|
-
await addToIndex(runtime, job.id);
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
async deleteJob(jobId) {
|
|
234
|
-
const componentType = getJobComponentType(jobId);
|
|
235
|
-
const existing = await runtime.getComponent(runtime.agentId, componentType);
|
|
236
|
-
if (!existing) {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
await runtime.deleteComponent(existing.id);
|
|
240
|
-
await removeFromIndex(runtime, jobId);
|
|
241
|
-
return true;
|
|
242
|
-
},
|
|
243
|
-
async listJobs(filter) {
|
|
244
|
-
const index = await getJobIndex(runtime);
|
|
245
|
-
const jobs = [];
|
|
246
|
-
for (const jobId of index.jobIds) {
|
|
247
|
-
const job = await this.getJob(jobId);
|
|
248
|
-
if (job && matchesFilter(job, filter)) {
|
|
249
|
-
jobs.push(job);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
jobs.sort((a, b) => {
|
|
253
|
-
const aNext = a.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
|
|
254
|
-
const bNext = b.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
|
|
255
|
-
return aNext - bNext;
|
|
256
|
-
});
|
|
257
|
-
return jobs;
|
|
258
|
-
},
|
|
259
|
-
async getJobCount() {
|
|
260
|
-
const index = await getJobIndex(runtime);
|
|
261
|
-
return index.jobIds.length;
|
|
262
|
-
},
|
|
263
|
-
async hasJob(jobId) {
|
|
264
|
-
const index = await getJobIndex(runtime);
|
|
265
|
-
return index.jobIds.includes(jobId);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// src/scheduler/timer-manager.ts
|
|
271
|
-
import { Cron as Cron2 } from "croner";
|
|
272
|
-
|
|
273
42
|
// src/scheduler/schedule-utils.ts
|
|
274
43
|
import { Cron } from "croner";
|
|
275
44
|
function parseTimestamp(timestamp) {
|
|
@@ -482,1007 +251,693 @@ function parseScheduleDescription(description, nowMs = Date.now()) {
|
|
|
482
251
|
return void 0;
|
|
483
252
|
}
|
|
484
253
|
|
|
485
|
-
// src/
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
this.onJobDue = onJobDue;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Starts the timer manager
|
|
498
|
-
*/
|
|
499
|
-
start() {
|
|
500
|
-
if (this.running) {
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
this.running = true;
|
|
504
|
-
this.startCheckInterval();
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Stops the timer manager and cleans up all timers
|
|
508
|
-
*/
|
|
509
|
-
stop() {
|
|
510
|
-
this.running = false;
|
|
511
|
-
this.stopCheckInterval();
|
|
512
|
-
this.clearAllJobs();
|
|
254
|
+
// src/actions/create-cron.ts
|
|
255
|
+
function parseNaturalLanguageRequest(text) {
|
|
256
|
+
const result = {};
|
|
257
|
+
const normalized = text.toLowerCase();
|
|
258
|
+
const everyMatch = /every\s+(\d+\s*(?:second|minute|hour|day|week)s?)/i.exec(
|
|
259
|
+
text
|
|
260
|
+
);
|
|
261
|
+
if (everyMatch) {
|
|
262
|
+
result.schedule = parseScheduleDescription(`every ${everyMatch[1]}`);
|
|
513
263
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
job,
|
|
525
|
-
executing: false
|
|
526
|
-
};
|
|
527
|
-
if (job.schedule.kind === "cron") {
|
|
528
|
-
const cronInstance = new Cron2(job.schedule.expr, {
|
|
529
|
-
timezone: job.schedule.tz?.trim() || void 0,
|
|
530
|
-
catch: true,
|
|
531
|
-
paused: true
|
|
532
|
-
// We manage firing manually via the check interval
|
|
533
|
-
});
|
|
534
|
-
tracked.cronInstance = cronInstance;
|
|
535
|
-
}
|
|
536
|
-
tracked.nextRunAtMs = job.state.nextRunAtMs ?? computeNextRunAtMs(job.schedule, Date.now());
|
|
537
|
-
this.trackedJobs.set(job.id, tracked);
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Removes a job from tracking
|
|
541
|
-
* @param jobId The job ID to remove
|
|
542
|
-
*/
|
|
543
|
-
untrackJob(jobId) {
|
|
544
|
-
const tracked = this.trackedJobs.get(jobId);
|
|
545
|
-
if (tracked) {
|
|
546
|
-
if (tracked.cronInstance) {
|
|
547
|
-
tracked.cronInstance.stop();
|
|
264
|
+
const atMatch = /at\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)/i.exec(text);
|
|
265
|
+
if (atMatch && !result.schedule) {
|
|
266
|
+
const timeStr = atMatch[1].toLowerCase();
|
|
267
|
+
let hours;
|
|
268
|
+
let minutes = 0;
|
|
269
|
+
const timeParts = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(timeStr);
|
|
270
|
+
if (timeParts) {
|
|
271
|
+
hours = parseInt(timeParts[1], 10);
|
|
272
|
+
if (timeParts[2]) {
|
|
273
|
+
minutes = parseInt(timeParts[2], 10);
|
|
548
274
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
275
|
+
if (timeParts[3]) {
|
|
276
|
+
if (timeParts[3].toLowerCase() === "pm" && hours !== 12) {
|
|
277
|
+
hours += 12;
|
|
278
|
+
} else if (timeParts[3].toLowerCase() === "am" && hours === 12) {
|
|
279
|
+
hours = 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (normalized.includes("daily") || normalized.includes("every day")) {
|
|
283
|
+
result.schedule = {
|
|
284
|
+
kind: "cron",
|
|
285
|
+
expr: `${minutes} ${hours} * * *`
|
|
286
|
+
};
|
|
287
|
+
} else if (normalized.includes("weekday") || normalized.includes("monday to friday")) {
|
|
288
|
+
result.schedule = {
|
|
289
|
+
kind: "cron",
|
|
290
|
+
expr: `${minutes} ${hours} * * 1-5`
|
|
291
|
+
};
|
|
292
|
+
} else if (normalized.includes("weekend")) {
|
|
293
|
+
result.schedule = {
|
|
294
|
+
kind: "cron",
|
|
295
|
+
expr: `${minutes} ${hours} * * 0,6`
|
|
296
|
+
};
|
|
297
|
+
} else {
|
|
298
|
+
result.schedule = {
|
|
299
|
+
kind: "cron",
|
|
300
|
+
expr: `${minutes} ${hours} * * *`
|
|
301
|
+
};
|
|
574
302
|
}
|
|
575
303
|
}
|
|
576
304
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
* @returns Next run time in ms, or undefined
|
|
581
|
-
*/
|
|
582
|
-
getNextRunAtMs(jobId) {
|
|
583
|
-
return this.trackedJobs.get(jobId)?.nextRunAtMs;
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Gets all tracked job IDs
|
|
587
|
-
*/
|
|
588
|
-
getTrackedJobIds() {
|
|
589
|
-
return Array.from(this.trackedJobs.keys());
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Gets the count of tracked jobs
|
|
593
|
-
*/
|
|
594
|
-
getTrackedJobCount() {
|
|
595
|
-
return this.trackedJobs.size;
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Checks if a specific job is currently executing
|
|
599
|
-
*/
|
|
600
|
-
isJobExecuting(jobId) {
|
|
601
|
-
return this.trackedJobs.get(jobId)?.executing ?? false;
|
|
305
|
+
const inMatch = /in\s+(\d+\s*(?:second|minute|hour|day)s?)/i.exec(text);
|
|
306
|
+
if (inMatch && !result.schedule) {
|
|
307
|
+
result.schedule = parseScheduleDescription(`in ${inMatch[1]}`);
|
|
602
308
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
309
|
+
const toMatch = /(?:to|that)\s+(.+?)(?:\s+every|\s+at|\s+in\s+\d|$)/i.exec(
|
|
310
|
+
text
|
|
311
|
+
);
|
|
312
|
+
if (toMatch) {
|
|
313
|
+
result.prompt = toMatch[1].trim();
|
|
314
|
+
result.name = toMatch[1].slice(0, 50).trim();
|
|
610
315
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
startCheckInterval() {
|
|
615
|
-
if (this.checkInterval) {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const intervalMs = this.config.timerCheckIntervalMs ?? DEFAULT_CRON_CONFIG.timerCheckIntervalMs;
|
|
619
|
-
this.checkInterval = setInterval(() => {
|
|
620
|
-
this.performCheck();
|
|
621
|
-
}, intervalMs);
|
|
622
|
-
if (this.checkInterval.unref) {
|
|
623
|
-
this.checkInterval.unref();
|
|
624
|
-
}
|
|
316
|
+
const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
|
|
317
|
+
if (nameMatch) {
|
|
318
|
+
result.name = nameMatch[1].trim();
|
|
625
319
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
stopCheckInterval() {
|
|
630
|
-
if (this.checkInterval) {
|
|
631
|
-
clearInterval(this.checkInterval);
|
|
632
|
-
this.checkInterval = null;
|
|
633
|
-
}
|
|
320
|
+
if (!result.name && result.schedule) {
|
|
321
|
+
const scheduleDesc = formatSchedule(result.schedule);
|
|
322
|
+
result.name = `Cron job (${scheduleDesc})`;
|
|
634
323
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
function formatJobResponse(job) {
|
|
327
|
+
const scheduleStr = formatSchedule(job.schedule);
|
|
328
|
+
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
|
|
329
|
+
return `Created cron job "${job.name}"
|
|
330
|
+
- ID: ${job.id}
|
|
331
|
+
- Schedule: ${scheduleStr}
|
|
332
|
+
- Status: ${job.enabled ? "enabled" : "disabled"}
|
|
333
|
+
- Next run: ${nextRun}`;
|
|
334
|
+
}
|
|
335
|
+
var createCronAction = {
|
|
336
|
+
name: CronActions.CREATE_CRON,
|
|
337
|
+
similes: [
|
|
338
|
+
"SCHEDULE_CRON",
|
|
339
|
+
"ADD_CRON",
|
|
340
|
+
"NEW_CRON",
|
|
341
|
+
"CREATE_SCHEDULED_JOB",
|
|
342
|
+
"SET_UP_CRON",
|
|
343
|
+
"SCHEDULE_JOB",
|
|
344
|
+
"CREATE_RECURRING_JOB"
|
|
345
|
+
],
|
|
346
|
+
description: "Creates a new cron job that runs on a schedule. Supports interval-based schedules (every X minutes), cron expressions, and one-time schedules.",
|
|
347
|
+
validate: async (_runtime, message) => {
|
|
348
|
+
const text = message.content?.text?.toLowerCase() ?? "";
|
|
349
|
+
const hasScheduleKeyword = text.includes("cron") || text.includes("schedule") || text.includes("every ") || text.includes("recurring") || text.includes("repeat") || text.includes("daily") || text.includes("hourly") || text.includes("weekly");
|
|
350
|
+
const hasCreateIntent = text.includes("create") || text.includes("add") || text.includes("set up") || text.includes("schedule") || text.includes("make");
|
|
351
|
+
return hasScheduleKeyword && hasCreateIntent;
|
|
352
|
+
},
|
|
353
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
354
|
+
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
355
|
+
if (!cronService) {
|
|
356
|
+
await callback?.({
|
|
357
|
+
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
358
|
+
});
|
|
359
|
+
return { success: false, error: "Cron service not available" };
|
|
651
360
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
361
|
+
const text = message.content?.text ?? "";
|
|
362
|
+
if (options?.jobInput && typeof options.jobInput === "object") {
|
|
363
|
+
const input = options.jobInput;
|
|
364
|
+
const scheduleError = validateSchedule(
|
|
365
|
+
input.schedule,
|
|
366
|
+
DEFAULT_CRON_CONFIG
|
|
367
|
+
);
|
|
368
|
+
if (scheduleError) {
|
|
369
|
+
await callback?.({
|
|
370
|
+
text: `Invalid schedule: ${scheduleError}`
|
|
658
371
|
});
|
|
372
|
+
return { success: false, error: scheduleError };
|
|
659
373
|
}
|
|
374
|
+
const job2 = await cronService.createJob(input);
|
|
375
|
+
await callback?.({
|
|
376
|
+
text: formatJobResponse(job2)
|
|
377
|
+
});
|
|
378
|
+
return { success: true, data: { jobId: job2.id, job: job2 } };
|
|
660
379
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
380
|
+
const parsed = parseNaturalLanguageRequest(text);
|
|
381
|
+
if (!parsed.schedule) {
|
|
382
|
+
await callback?.({
|
|
383
|
+
text: `I couldn't understand the schedule. Please specify when the job should run, for example:
|
|
384
|
+
- "every 5 minutes"
|
|
385
|
+
- "every hour"
|
|
386
|
+
- "daily at 9am"
|
|
387
|
+
- "every weekday at 8:30am"`
|
|
388
|
+
});
|
|
389
|
+
return { success: false, error: "Could not parse schedule" };
|
|
670
390
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
async function withEnforcedTimeout(operation, timeoutMs, signal) {
|
|
688
|
-
if (signal.aborted) {
|
|
689
|
-
throw new Error("Job execution timeout");
|
|
690
|
-
}
|
|
691
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
692
|
-
const timeoutId = setTimeout(() => {
|
|
693
|
-
reject(new Error("Job execution timeout"));
|
|
694
|
-
}, timeoutMs);
|
|
695
|
-
signal.addEventListener("abort", () => {
|
|
696
|
-
clearTimeout(timeoutId);
|
|
697
|
-
reject(new Error("Job execution timeout"));
|
|
391
|
+
const jobInput = {
|
|
392
|
+
name: parsed.name || "Unnamed cron job",
|
|
393
|
+
description: parsed.description,
|
|
394
|
+
enabled: true,
|
|
395
|
+
schedule: parsed.schedule,
|
|
396
|
+
payload: {
|
|
397
|
+
kind: "prompt",
|
|
398
|
+
text: parsed.prompt || "Run scheduled task"
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
if (parsed.schedule.kind === "at") {
|
|
402
|
+
jobInput.deleteAfterRun = true;
|
|
403
|
+
}
|
|
404
|
+
const job = await cronService.createJob(jobInput);
|
|
405
|
+
await callback?.({
|
|
406
|
+
text: formatJobResponse(job)
|
|
698
407
|
});
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
data: {
|
|
411
|
+
jobId: job.id,
|
|
412
|
+
job
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
examples: [
|
|
417
|
+
[
|
|
418
|
+
{
|
|
419
|
+
name: "{{user1}}",
|
|
420
|
+
content: { text: "Create a cron job to check the news every hour" }
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "{{agentName}}",
|
|
424
|
+
content: {
|
|
425
|
+
text: 'Created cron job "check the news"\n- ID: abc-123\n- Schedule: every 1 hour\n- Status: enabled\n- Next run: in 1 hour'
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
[
|
|
430
|
+
{
|
|
431
|
+
name: "{{user1}}",
|
|
432
|
+
content: {
|
|
433
|
+
text: "Schedule a daily reminder at 9am to review my goals"
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "{{agentName}}",
|
|
438
|
+
content: {
|
|
439
|
+
text: 'Created cron job "review my goals"\n- ID: def-456\n- Schedule: cron: 0 9 * * *\n- Status: enabled\n- Next run: tomorrow at 9:00 AM'
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
[
|
|
444
|
+
{
|
|
445
|
+
name: "{{user1}}",
|
|
446
|
+
content: {
|
|
447
|
+
text: "Set up a recurring job every 5 minutes to check server status"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "{{agentName}}",
|
|
452
|
+
content: {
|
|
453
|
+
text: 'Created cron job "check server status"\n- ID: ghi-789\n- Schedule: every 5 minutes\n- Status: enabled\n- Next run: in 5 minutes'
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
]
|
|
458
|
+
};
|
|
705
459
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return result;
|
|
711
|
-
}
|
|
712
|
-
async function executeActionPayload(runtime, payload, context) {
|
|
713
|
-
const actions = runtime.actions ?? [];
|
|
714
|
-
const action = actions.find(
|
|
715
|
-
(a) => a.name.toLowerCase() === payload.actionName.toLowerCase()
|
|
460
|
+
// src/actions/delete-cron.ts
|
|
461
|
+
function extractJobIdentifier(text) {
|
|
462
|
+
const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(
|
|
463
|
+
text
|
|
716
464
|
);
|
|
717
|
-
if (
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
const roomId = payload.roomId || runtime.agentId;
|
|
721
|
-
const memory = {
|
|
722
|
-
id: uuidv42(),
|
|
723
|
-
entityId: runtime.agentId,
|
|
724
|
-
roomId,
|
|
725
|
-
agentId: runtime.agentId,
|
|
726
|
-
content: {
|
|
727
|
-
text: `[Cron Job: ${context.job.name}] Executing action: ${payload.actionName}`,
|
|
728
|
-
// Spread params into content for actions to access
|
|
729
|
-
...payload.params
|
|
730
|
-
},
|
|
731
|
-
createdAt: Date.now()
|
|
732
|
-
};
|
|
733
|
-
const callbackResponses = [];
|
|
734
|
-
const callback = async (response) => {
|
|
735
|
-
if (response.text) {
|
|
736
|
-
callbackResponses.push(response.text);
|
|
737
|
-
}
|
|
738
|
-
return [];
|
|
739
|
-
};
|
|
740
|
-
const isValid = await action.validate(runtime, memory, void 0);
|
|
741
|
-
if (!isValid) {
|
|
742
|
-
throw new Error(`Action validation failed: ${payload.actionName}`);
|
|
465
|
+
if (idMatch) {
|
|
466
|
+
return { id: idMatch[1] };
|
|
743
467
|
}
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
outputParts.push(...callbackResponses);
|
|
468
|
+
const quotedMatch = /["']([^"']+)["']/i.exec(text);
|
|
469
|
+
if (quotedMatch) {
|
|
470
|
+
return { name: quotedMatch[1] };
|
|
748
471
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
} else if (typeof handlerResult === "object") {
|
|
753
|
-
const result = handlerResult;
|
|
754
|
-
if (result.text && typeof result.text === "string") {
|
|
755
|
-
outputParts.push(result.text);
|
|
756
|
-
} else if (result.success !== void 0 || result.data !== void 0) {
|
|
757
|
-
outputParts.push(JSON.stringify(handlerResult));
|
|
758
|
-
}
|
|
759
|
-
}
|
|
472
|
+
const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
|
|
473
|
+
if (namedMatch) {
|
|
474
|
+
return { name: namedMatch[1] };
|
|
760
475
|
}
|
|
761
|
-
return
|
|
762
|
-
}
|
|
763
|
-
async function executeEventPayload(runtime, payload, context) {
|
|
764
|
-
const eventPayload = {
|
|
765
|
-
runtime,
|
|
766
|
-
source: `cron:${context.job.id}`,
|
|
767
|
-
cronJob: {
|
|
768
|
-
id: context.job.id,
|
|
769
|
-
name: context.job.name
|
|
770
|
-
},
|
|
771
|
-
...payload.payload || {}
|
|
772
|
-
};
|
|
773
|
-
await runtime.emitEvent(payload.eventName, eventPayload);
|
|
774
|
-
return `Event ${payload.eventName} emitted`;
|
|
476
|
+
return {};
|
|
775
477
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
478
|
+
var deleteCronAction = {
|
|
479
|
+
name: CronActions.DELETE_CRON,
|
|
480
|
+
similes: [
|
|
481
|
+
"REMOVE_CRON",
|
|
482
|
+
"CANCEL_CRON",
|
|
483
|
+
"STOP_CRON",
|
|
484
|
+
"DELETE_SCHEDULED_JOB",
|
|
485
|
+
"REMOVE_SCHEDULED_JOB"
|
|
486
|
+
],
|
|
487
|
+
description: "Deletes a cron job by ID or name, removing it from the schedule permanently.",
|
|
488
|
+
validate: async (_runtime, message) => {
|
|
489
|
+
const text = message.content?.text?.toLowerCase() ?? "";
|
|
490
|
+
const hasDeleteKeyword = text.includes("delete") || text.includes("remove") || text.includes("cancel") || text.includes("stop") && !text.includes("stop running");
|
|
491
|
+
const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
|
|
492
|
+
return hasDeleteKeyword && hasCronKeyword;
|
|
493
|
+
},
|
|
494
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
495
|
+
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
496
|
+
if (!cronService) {
|
|
497
|
+
await callback?.({
|
|
498
|
+
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
499
|
+
});
|
|
500
|
+
return { success: false, error: "Cron service not available" };
|
|
798
501
|
}
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
502
|
+
const text = message.content?.text ?? "";
|
|
503
|
+
let jobId = options?.jobId;
|
|
504
|
+
let jobName;
|
|
505
|
+
if (!jobId) {
|
|
506
|
+
const identifier = extractJobIdentifier(text);
|
|
507
|
+
if (identifier.id) {
|
|
508
|
+
jobId = identifier.id;
|
|
509
|
+
} else if (identifier.name) {
|
|
510
|
+
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
511
|
+
const job = jobs.find(
|
|
512
|
+
(j) => j.name.toLowerCase() === identifier.name?.toLowerCase()
|
|
513
|
+
);
|
|
514
|
+
if (!job) {
|
|
515
|
+
await callback?.({
|
|
516
|
+
text: `No cron job found with name: ${identifier.name}`
|
|
517
|
+
});
|
|
518
|
+
return { success: false, error: "Job not found" };
|
|
810
519
|
}
|
|
520
|
+
jobId = job.id;
|
|
521
|
+
jobName = job.name;
|
|
811
522
|
}
|
|
812
|
-
};
|
|
813
|
-
output = await withEnforcedTimeout(executeOperation, timeoutMs, controller.signal);
|
|
814
|
-
} catch (err) {
|
|
815
|
-
if (err instanceof Error) {
|
|
816
|
-
if (err.message === "Job execution timeout" || controller.signal.aborted) {
|
|
817
|
-
status = "timeout";
|
|
818
|
-
error = "Execution timed out";
|
|
819
|
-
} else {
|
|
820
|
-
status = "error";
|
|
821
|
-
error = err.message;
|
|
822
|
-
}
|
|
823
|
-
} else {
|
|
824
|
-
status = "error";
|
|
825
|
-
error = String(err);
|
|
826
523
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
status,
|
|
833
|
-
durationMs,
|
|
834
|
-
output,
|
|
835
|
-
error
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
function validateJobExecutability(runtime, job) {
|
|
839
|
-
const { payload } = job;
|
|
840
|
-
const payloadRecord = payload;
|
|
841
|
-
if (isOttoPayload(payloadRecord)) {
|
|
842
|
-
if (payloadRecord.kind === "systemEvent") {
|
|
843
|
-
const text = typeof payloadRecord.text === "string" ? payloadRecord.text.trim() : "";
|
|
844
|
-
return text ? null : "systemEvent payload must have non-empty text";
|
|
845
|
-
}
|
|
846
|
-
if (payloadRecord.kind === "agentTurn") {
|
|
847
|
-
const message = typeof payloadRecord.message === "string" ? payloadRecord.message.trim() : "";
|
|
848
|
-
return message ? null : "agentTurn payload must have non-empty message";
|
|
524
|
+
if (!jobId) {
|
|
525
|
+
await callback?.({
|
|
526
|
+
text: 'Please specify which cron job to delete. You can use the job ID or name.\nExample: "delete cron job abc-123" or "remove cron called daily-check"'
|
|
527
|
+
});
|
|
528
|
+
return { success: false, error: "No job identifier provided" };
|
|
849
529
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const text = payload.text?.trim();
|
|
855
|
-
if (!text) {
|
|
856
|
-
return "Prompt payload must have non-empty text";
|
|
857
|
-
}
|
|
858
|
-
if (typeof runtime.useModel !== "function") {
|
|
859
|
-
return "Runtime does not support useModel for prompt execution";
|
|
530
|
+
if (!jobName) {
|
|
531
|
+
const job = await cronService.getJob(jobId);
|
|
532
|
+
if (job) {
|
|
533
|
+
jobName = job.name;
|
|
860
534
|
}
|
|
861
|
-
return null;
|
|
862
535
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
536
|
+
const deleted = await cronService.deleteJob(jobId);
|
|
537
|
+
if (!deleted) {
|
|
538
|
+
await callback?.({
|
|
539
|
+
text: `No cron job found with ID: ${jobId}`
|
|
540
|
+
});
|
|
541
|
+
return { success: false, error: "Job not found" };
|
|
542
|
+
}
|
|
543
|
+
await callback?.({
|
|
544
|
+
text: `Deleted cron job "${jobName || "unknown"}" (${jobId}).
|
|
545
|
+
The job has been permanently removed and will no longer run.`
|
|
546
|
+
});
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
data: {
|
|
550
|
+
jobId,
|
|
551
|
+
jobName,
|
|
552
|
+
deleted: true
|
|
867
553
|
}
|
|
868
|
-
|
|
869
|
-
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
examples: [
|
|
557
|
+
[
|
|
558
|
+
{
|
|
559
|
+
name: "{{user1}}",
|
|
560
|
+
content: { text: "Delete the cron job called daily-check" }
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "{{agentName}}",
|
|
564
|
+
content: {
|
|
565
|
+
text: 'Deleted cron job "daily-check" (abc-123).\nThe job has been permanently removed and will no longer run.'
|
|
566
|
+
}
|
|
870
567
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
568
|
+
],
|
|
569
|
+
[
|
|
570
|
+
{
|
|
571
|
+
name: "{{user1}}",
|
|
572
|
+
content: { text: "Remove cron abc-123-def-456" }
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "{{agentName}}",
|
|
576
|
+
content: {
|
|
577
|
+
text: 'Deleted cron job "hourly-status" (abc-123-def-456).\nThe job has been permanently removed and will no longer run.'
|
|
578
|
+
}
|
|
877
579
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
580
|
+
]
|
|
581
|
+
]
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/actions/list-crons.ts
|
|
585
|
+
function formatJobList(jobs, includeDisabled) {
|
|
586
|
+
if (jobs.length === 0) {
|
|
587
|
+
return includeDisabled ? "No cron jobs found." : 'No active cron jobs found. Use "list all crons" to include disabled jobs.';
|
|
588
|
+
}
|
|
589
|
+
const lines = [
|
|
590
|
+
`Found ${jobs.length} cron job${jobs.length === 1 ? "" : "s"}:
|
|
591
|
+
`
|
|
592
|
+
];
|
|
593
|
+
for (const job of jobs) {
|
|
594
|
+
const scheduleStr = formatSchedule(job.schedule);
|
|
595
|
+
const statusStr = job.enabled ? "enabled" : "disabled";
|
|
596
|
+
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
|
|
597
|
+
const lastStatus = job.state.lastStatus ? ` (last: ${job.state.lastStatus})` : "";
|
|
598
|
+
lines.push(
|
|
599
|
+
`\u2022 ${job.name}${lastStatus}
|
|
600
|
+
ID: ${job.id}
|
|
601
|
+
Schedule: ${scheduleStr}
|
|
602
|
+
Status: ${statusStr}
|
|
603
|
+
Next run: ${nextRun}
|
|
604
|
+
Runs: ${job.state.runCount} | Errors: ${job.state.errorCount}`
|
|
605
|
+
);
|
|
887
606
|
}
|
|
607
|
+
return lines.join("\n");
|
|
888
608
|
}
|
|
609
|
+
function formatJobDetails(job) {
|
|
610
|
+
const scheduleStr = formatSchedule(job.schedule);
|
|
611
|
+
const statusStr = job.enabled ? "enabled" : "disabled";
|
|
612
|
+
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
|
|
613
|
+
const lastRun = job.state.lastRunAtMs ? new Date(job.state.lastRunAtMs).toLocaleString() : "never";
|
|
614
|
+
let details = `Cron Job: ${job.name}
|
|
889
615
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
timerManager;
|
|
897
|
-
initialized = false;
|
|
898
|
-
constructor(runtime, config) {
|
|
899
|
-
super(runtime);
|
|
900
|
-
this.cronConfig = { ...DEFAULT_CRON_CONFIG, ...config };
|
|
616
|
+
`;
|
|
617
|
+
details += `ID: ${job.id}
|
|
618
|
+
`;
|
|
619
|
+
if (job.description) {
|
|
620
|
+
details += `Description: ${job.description}
|
|
621
|
+
`;
|
|
901
622
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
623
|
+
details += `
|
|
624
|
+
Schedule: ${scheduleStr}
|
|
625
|
+
`;
|
|
626
|
+
details += `Status: ${statusStr}
|
|
627
|
+
`;
|
|
628
|
+
if (job.deleteAfterRun) {
|
|
629
|
+
details += `Type: one-shot (will be deleted after successful run)
|
|
630
|
+
`;
|
|
909
631
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const nextRunAtMs = computeNextRunAtMs(job.schedule, nowMs);
|
|
925
|
-
if (job.state.nextRunAtMs !== nextRunAtMs) {
|
|
926
|
-
job.state.nextRunAtMs = nextRunAtMs;
|
|
927
|
-
await this.storage.saveJob(job);
|
|
928
|
-
}
|
|
929
|
-
if (job.enabled) {
|
|
930
|
-
this.timerManager.trackJob(job);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (this.cronConfig.catchUpMissedJobs) {
|
|
934
|
-
await this.handleMissedJobs(jobs);
|
|
935
|
-
}
|
|
936
|
-
this.timerManager.start();
|
|
937
|
-
await startHeartbeat(this.runtime);
|
|
938
|
-
this.initialized = true;
|
|
939
|
-
logger.info(
|
|
940
|
-
`[CronService] Started for agent ${this.runtime.agentId} with ${jobs.length} jobs`
|
|
941
|
-
);
|
|
632
|
+
details += `
|
|
633
|
+
Execution Stats:
|
|
634
|
+
`;
|
|
635
|
+
details += ` Next run: ${nextRun}
|
|
636
|
+
`;
|
|
637
|
+
details += ` Last run: ${lastRun}
|
|
638
|
+
`;
|
|
639
|
+
details += ` Total runs: ${job.state.runCount}
|
|
640
|
+
`;
|
|
641
|
+
details += ` Total errors: ${job.state.errorCount}
|
|
642
|
+
`;
|
|
643
|
+
if (job.state.lastStatus) {
|
|
644
|
+
details += ` Last status: ${job.state.lastStatus}
|
|
645
|
+
`;
|
|
942
646
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
async stop() {
|
|
947
|
-
if (this.timerManager) {
|
|
948
|
-
this.timerManager.stop();
|
|
949
|
-
}
|
|
950
|
-
this.initialized = false;
|
|
951
|
-
logger.info(`[CronService] Stopped for agent ${this.runtime.agentId}`);
|
|
647
|
+
if (job.state.lastError) {
|
|
648
|
+
details += ` Last error: ${job.state.lastError}
|
|
649
|
+
`;
|
|
952
650
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
getConfig() {
|
|
957
|
-
return { ...this.cronConfig };
|
|
651
|
+
if (job.state.lastDurationMs !== void 0) {
|
|
652
|
+
details += ` Last duration: ${job.state.lastDurationMs}ms
|
|
653
|
+
`;
|
|
958
654
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
throw new Error(`Invalid schedule: ${scheduleError}`);
|
|
972
|
-
}
|
|
973
|
-
const currentCount = await this.storage.getJobCount();
|
|
974
|
-
if (currentCount >= this.cronConfig.maxJobsPerAgent) {
|
|
975
|
-
throw new Error(
|
|
976
|
-
`Maximum jobs limit reached (${this.cronConfig.maxJobsPerAgent}). Delete some jobs before creating new ones.`
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
const nowMs = Date.now();
|
|
980
|
-
const job = {
|
|
981
|
-
id: uuidv43(),
|
|
982
|
-
name: input.name,
|
|
983
|
-
description: input.description,
|
|
984
|
-
// Explicitly default to true if not provided - jobs are enabled by default
|
|
985
|
-
enabled: input.enabled ?? true,
|
|
986
|
-
// Explicitly default to false - jobs persist after run by default
|
|
987
|
-
deleteAfterRun: input.deleteAfterRun ?? false,
|
|
988
|
-
createdAtMs: nowMs,
|
|
989
|
-
updatedAtMs: nowMs,
|
|
990
|
-
schedule: input.schedule,
|
|
991
|
-
payload: input.payload,
|
|
992
|
-
tags: input.tags,
|
|
993
|
-
metadata: input.metadata,
|
|
994
|
-
state: {
|
|
995
|
-
nextRunAtMs: computeNextRunAtMs(input.schedule, nowMs),
|
|
996
|
-
runCount: input.state?.runCount ?? 0,
|
|
997
|
-
errorCount: input.state?.errorCount ?? 0,
|
|
998
|
-
...input.state
|
|
999
|
-
}
|
|
1000
|
-
};
|
|
1001
|
-
const execError = validateJobExecutability(this.runtime, job);
|
|
1002
|
-
if (execError) {
|
|
1003
|
-
throw new Error(`Job cannot be executed: ${execError}`);
|
|
1004
|
-
}
|
|
1005
|
-
await this.storage.saveJob(job);
|
|
1006
|
-
if (job.enabled) {
|
|
1007
|
-
this.timerManager.trackJob(job);
|
|
1008
|
-
}
|
|
1009
|
-
await this.emitCronEvent(CronEvents.CRON_CREATED, job);
|
|
1010
|
-
logger.info(
|
|
1011
|
-
`[CronService] Created job "${job.name}" (${job.id}) - ${formatSchedule(job.schedule)}`
|
|
1012
|
-
);
|
|
1013
|
-
return job;
|
|
655
|
+
details += `
|
|
656
|
+
Payload Type: ${job.payload.kind}
|
|
657
|
+
`;
|
|
658
|
+
if (job.payload.kind === "prompt") {
|
|
659
|
+
details += `Prompt: ${job.payload.text.slice(0, 200)}${job.payload.text.length > 200 ? "..." : ""}
|
|
660
|
+
`;
|
|
661
|
+
} else if (job.payload.kind === "action") {
|
|
662
|
+
details += `Action: ${job.payload.actionName}
|
|
663
|
+
`;
|
|
664
|
+
} else if (job.payload.kind === "event") {
|
|
665
|
+
details += `Event: ${job.payload.eventName}
|
|
666
|
+
`;
|
|
1014
667
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
668
|
+
if (job.tags && job.tags.length > 0) {
|
|
669
|
+
details += `
|
|
670
|
+
Tags: ${job.tags.join(", ")}
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
details += `
|
|
674
|
+
Created: ${new Date(job.createdAtMs).toLocaleString()}
|
|
675
|
+
`;
|
|
676
|
+
details += `Updated: ${new Date(job.updatedAtMs).toLocaleString()}
|
|
677
|
+
`;
|
|
678
|
+
return details;
|
|
679
|
+
}
|
|
680
|
+
var listCronsAction = {
|
|
681
|
+
name: CronActions.LIST_CRONS,
|
|
682
|
+
similes: [
|
|
683
|
+
"SHOW_CRONS",
|
|
684
|
+
"GET_CRONS",
|
|
685
|
+
"VIEW_CRONS",
|
|
686
|
+
"LIST_SCHEDULED_JOBS",
|
|
687
|
+
"SHOW_SCHEDULED_JOBS",
|
|
688
|
+
"MY_CRONS",
|
|
689
|
+
"CRON_STATUS"
|
|
690
|
+
],
|
|
691
|
+
description: "Lists all cron jobs. Can filter by enabled status or show details of a specific job.",
|
|
692
|
+
validate: async (_runtime, message) => {
|
|
693
|
+
const text = message.content?.text?.toLowerCase() ?? "";
|
|
694
|
+
const hasListKeyword = text.includes("list") || text.includes("show") || text.includes("view") || text.includes("get") || text.includes("what");
|
|
695
|
+
const hasCronKeyword = text.includes("cron") || text.includes("scheduled") || text.includes("job") || text.includes("schedule");
|
|
696
|
+
return hasListKeyword && hasCronKeyword;
|
|
697
|
+
},
|
|
698
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
699
|
+
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
700
|
+
if (!cronService) {
|
|
701
|
+
await callback?.({
|
|
702
|
+
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
703
|
+
});
|
|
704
|
+
return { success: false, error: "Cron service not available" };
|
|
1026
705
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
706
|
+
const text = message.content?.text?.toLowerCase() ?? "";
|
|
707
|
+
const idMatch = /(?:job|cron)\s+([a-f0-9-]{36})/i.exec(text);
|
|
708
|
+
if (idMatch) {
|
|
709
|
+
const jobId = idMatch[1];
|
|
710
|
+
const job = await cronService.getJob(jobId);
|
|
711
|
+
if (!job) {
|
|
712
|
+
await callback?.({
|
|
713
|
+
text: `No cron job found with ID: ${jobId}`
|
|
714
|
+
});
|
|
715
|
+
return { success: false, error: "Job not found" };
|
|
1031
716
|
}
|
|
717
|
+
await callback?.({
|
|
718
|
+
text: formatJobDetails(job)
|
|
719
|
+
});
|
|
720
|
+
return { success: true, data: { job } };
|
|
1032
721
|
}
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
...existing.state,
|
|
1044
|
-
...patch.state
|
|
722
|
+
const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
|
|
723
|
+
if (nameMatch) {
|
|
724
|
+
const jobName = nameMatch[1].toLowerCase();
|
|
725
|
+
const jobs2 = await cronService.listJobs({ includeDisabled: true });
|
|
726
|
+
const job = jobs2.find((j) => j.name.toLowerCase().includes(jobName));
|
|
727
|
+
if (!job) {
|
|
728
|
+
await callback?.({
|
|
729
|
+
text: `No cron job found with name containing: ${jobName}`
|
|
730
|
+
});
|
|
731
|
+
return { success: false, error: "Job not found" };
|
|
1045
732
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (patch.enabled === false) {
|
|
1051
|
-
updated.state.nextRunAtMs = void 0;
|
|
1052
|
-
updated.state.runningAtMs = void 0;
|
|
733
|
+
await callback?.({
|
|
734
|
+
text: formatJobDetails(job)
|
|
735
|
+
});
|
|
736
|
+
return { success: true, data: { job } };
|
|
1053
737
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
throw new Error(`Job cannot be executed: ${execError}`);
|
|
1058
|
-
}
|
|
738
|
+
const filter = {};
|
|
739
|
+
if (text.includes("all")) {
|
|
740
|
+
filter.includeDisabled = true;
|
|
1059
741
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
} else {
|
|
1064
|
-
this.timerManager.untrackJob(jobId);
|
|
742
|
+
if (text.includes("enabled") || text.includes("active")) {
|
|
743
|
+
filter.enabled = true;
|
|
744
|
+
filter.includeDisabled = false;
|
|
1065
745
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Deletes a cron job
|
|
1072
|
-
* @param jobId The job ID to delete
|
|
1073
|
-
* @returns true if deleted, false if not found
|
|
1074
|
-
*/
|
|
1075
|
-
async deleteJob(jobId) {
|
|
1076
|
-
const existing = await this.storage.getJob(jobId);
|
|
1077
|
-
if (!existing) {
|
|
1078
|
-
return false;
|
|
746
|
+
if (text.includes("disabled") || text.includes("inactive")) {
|
|
747
|
+
filter.enabled = false;
|
|
748
|
+
filter.includeDisabled = true;
|
|
1079
749
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (deleted) {
|
|
1083
|
-
await this.emitCronEvent(CronEvents.CRON_DELETED, existing);
|
|
1084
|
-
logger.info(`[CronService] Deleted job "${existing.name}" (${existing.id})`);
|
|
750
|
+
if (options?.filter && typeof options.filter === "object") {
|
|
751
|
+
Object.assign(filter, options.filter);
|
|
1085
752
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
753
|
+
const jobs = await cronService.listJobs(filter);
|
|
754
|
+
await callback?.({
|
|
755
|
+
text: formatJobList(jobs, filter.includeDisabled ?? false)
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
success: true,
|
|
759
|
+
data: {
|
|
760
|
+
jobs,
|
|
761
|
+
count: jobs.length
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
},
|
|
765
|
+
examples: [
|
|
766
|
+
[
|
|
767
|
+
{
|
|
768
|
+
name: "{{user1}}",
|
|
769
|
+
content: { text: "List my cron jobs" }
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "{{agentName}}",
|
|
773
|
+
content: {
|
|
774
|
+
text: "Found 2 cron jobs:\n\n\u2022 Daily news check\n ID: abc-123\n Schedule: cron: 0 9 * * *\n Status: enabled\n Next run: tomorrow at 9:00 AM\n Runs: 5 | Errors: 0\n\n\u2022 Hourly status check\n ID: def-456\n Schedule: every 1 hour\n Status: enabled\n Next run: in 45 minutes\n Runs: 120 | Errors: 2"
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
],
|
|
778
|
+
[
|
|
779
|
+
{
|
|
780
|
+
name: "{{user1}}",
|
|
781
|
+
content: { text: "Show all crons including disabled" }
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "{{agentName}}",
|
|
785
|
+
content: {
|
|
786
|
+
text: "Found 3 cron jobs:\n\n\u2022 Daily news check\n ID: abc-123\n Schedule: cron: 0 9 * * *\n Status: enabled\n ...\n\n\u2022 Old backup job\n ID: xyz-789\n Schedule: every 1 day\n Status: disabled\n ..."
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
]
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/actions/run-cron.ts
|
|
794
|
+
function extractJobIdentifier2(text) {
|
|
795
|
+
const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(
|
|
796
|
+
text
|
|
797
|
+
);
|
|
798
|
+
if (idMatch) {
|
|
799
|
+
return { id: idMatch[1] };
|
|
1095
800
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
* @returns Array of matching jobs
|
|
1100
|
-
*/
|
|
1101
|
-
async listJobs(filter) {
|
|
1102
|
-
return this.storage.listJobs(filter);
|
|
801
|
+
const quotedMatch = /["']([^"']+)["']/i.exec(text);
|
|
802
|
+
if (quotedMatch) {
|
|
803
|
+
return { name: quotedMatch[1] };
|
|
1103
804
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
async getJobCount() {
|
|
1108
|
-
return this.storage.getJobCount();
|
|
805
|
+
const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
|
|
806
|
+
if (namedMatch) {
|
|
807
|
+
return { name: namedMatch[1] };
|
|
1109
808
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
809
|
+
return {};
|
|
810
|
+
}
|
|
811
|
+
var runCronAction = {
|
|
812
|
+
name: CronActions.RUN_CRON,
|
|
813
|
+
similes: [
|
|
814
|
+
"EXECUTE_CRON",
|
|
815
|
+
"TRIGGER_CRON",
|
|
816
|
+
"FIRE_CRON",
|
|
817
|
+
"RUN_SCHEDULED_JOB",
|
|
818
|
+
"EXECUTE_JOB",
|
|
819
|
+
"TRIGGER_JOB"
|
|
820
|
+
],
|
|
821
|
+
description: "Manually runs a cron job immediately, regardless of its schedule. Useful for testing or one-off execution.",
|
|
822
|
+
validate: async (_runtime, message) => {
|
|
823
|
+
const text = message.content?.text?.toLowerCase() ?? "";
|
|
824
|
+
const hasRunKeyword = text.includes("run") || text.includes("execute") || text.includes("trigger") || text.includes("fire");
|
|
825
|
+
const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
|
|
826
|
+
const isCreateIntent = text.includes("run every") || text.includes("runs every");
|
|
827
|
+
return hasRunKeyword && hasCronKeyword && !isCreateIntent;
|
|
828
|
+
},
|
|
829
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
830
|
+
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
831
|
+
if (!cronService) {
|
|
832
|
+
await callback?.({
|
|
833
|
+
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
834
|
+
});
|
|
835
|
+
return { success: false, error: "Cron service not available" };
|
|
1123
836
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
837
|
+
const text = message.content?.text ?? "";
|
|
838
|
+
let jobId = options?.jobId;
|
|
839
|
+
let jobName;
|
|
840
|
+
if (!jobId) {
|
|
841
|
+
const identifier = extractJobIdentifier2(text);
|
|
842
|
+
if (identifier.id) {
|
|
843
|
+
jobId = identifier.id;
|
|
844
|
+
} else if (identifier.name) {
|
|
845
|
+
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
846
|
+
const job = jobs.find(
|
|
847
|
+
(j) => j.name.toLowerCase() === identifier.name?.toLowerCase()
|
|
848
|
+
);
|
|
849
|
+
if (!job) {
|
|
850
|
+
await callback?.({
|
|
851
|
+
text: `No cron job found with name: ${identifier.name}`
|
|
852
|
+
});
|
|
853
|
+
return { success: false, error: "Job not found" };
|
|
854
|
+
}
|
|
855
|
+
jobId = job.id;
|
|
856
|
+
jobName = job.name;
|
|
1134
857
|
}
|
|
1135
858
|
}
|
|
1136
|
-
if (!
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
error: "Job is disabled"
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
const result = await this.executeJobInternal(job);
|
|
1145
|
-
return {
|
|
1146
|
-
ran: true,
|
|
1147
|
-
...result
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
// ============================================================================
|
|
1151
|
-
// INTERNAL METHODS
|
|
1152
|
-
// ============================================================================
|
|
1153
|
-
/**
|
|
1154
|
-
* Handles a job becoming due (called by timer manager)
|
|
1155
|
-
*/
|
|
1156
|
-
async handleJobDue(jobId) {
|
|
1157
|
-
const job = await this.storage.getJob(jobId);
|
|
1158
|
-
if (!job) {
|
|
1159
|
-
this.timerManager.untrackJob(jobId);
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (!job.enabled) {
|
|
1163
|
-
this.timerManager.untrackJob(jobId);
|
|
1164
|
-
return;
|
|
859
|
+
if (!jobId) {
|
|
860
|
+
await callback?.({
|
|
861
|
+
text: 'Please specify which cron job to run. You can use the job ID or name.\nExample: "run cron job abc-123" or "execute cron called daily-check"'
|
|
862
|
+
});
|
|
863
|
+
return { success: false, error: "No job identifier provided" };
|
|
1165
864
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
async executeJobInternal(job) {
|
|
1172
|
-
const nowMs = Date.now();
|
|
1173
|
-
job.state.runningAtMs = nowMs;
|
|
1174
|
-
await this.storage.saveJob(job);
|
|
1175
|
-
logger.debug(`[CronService] Executing job "${job.name}" (${job.id})`);
|
|
1176
|
-
const result = await executeJob(this.runtime, job, this.cronConfig);
|
|
1177
|
-
job.state.runningAtMs = void 0;
|
|
1178
|
-
job.state.lastRunAtMs = nowMs;
|
|
1179
|
-
job.state.lastStatus = result.status;
|
|
1180
|
-
job.state.lastDurationMs = result.durationMs;
|
|
1181
|
-
if (result.status === "ok") {
|
|
1182
|
-
job.state.runCount += 1;
|
|
1183
|
-
job.state.lastError = void 0;
|
|
1184
|
-
} else {
|
|
1185
|
-
job.state.errorCount += 1;
|
|
1186
|
-
job.state.lastError = result.error;
|
|
865
|
+
if (!jobName) {
|
|
866
|
+
const job = await cronService.getJob(jobId);
|
|
867
|
+
if (job) {
|
|
868
|
+
jobName = job.name;
|
|
869
|
+
}
|
|
1187
870
|
}
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
logger.info(
|
|
1195
|
-
`[CronService] Deleted one-shot job "${job.name}" (${job.id}) after successful execution`
|
|
1196
|
-
);
|
|
1197
|
-
} else {
|
|
1198
|
-
await this.storage.saveJob(job);
|
|
1199
|
-
this.timerManager.markFinished(job.id, job);
|
|
871
|
+
const result = await cronService.runJob(jobId, "force");
|
|
872
|
+
if (!result.ran) {
|
|
873
|
+
await callback?.({
|
|
874
|
+
text: `Could not run job: ${result.error}`
|
|
875
|
+
});
|
|
876
|
+
return { success: false, error: result.error };
|
|
1200
877
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const nowMs = Date.now();
|
|
1213
|
-
const windowStart = nowMs - this.cronConfig.catchUpWindowMs;
|
|
1214
|
-
for (const job of jobs) {
|
|
1215
|
-
if (!job.enabled) {
|
|
1216
|
-
continue;
|
|
1217
|
-
}
|
|
1218
|
-
const lastRunAtMs = job.state.lastRunAtMs ?? 0;
|
|
1219
|
-
const nextRunAtMs = job.state.nextRunAtMs;
|
|
1220
|
-
if (nextRunAtMs && nextRunAtMs >= windowStart && nextRunAtMs < nowMs && lastRunAtMs < nextRunAtMs) {
|
|
1221
|
-
logger.info(
|
|
1222
|
-
`[CronService] Catching up missed job "${job.name}" (${job.id}) that was due at ${new Date(nextRunAtMs).toISOString()}`
|
|
1223
|
-
);
|
|
1224
|
-
await this.executeJobInternal(job);
|
|
1225
|
-
}
|
|
878
|
+
let responseText = `Ran cron job "${jobName || "unknown"}" (${jobId})
|
|
879
|
+
`;
|
|
880
|
+
responseText += `Status: ${result.status}
|
|
881
|
+
`;
|
|
882
|
+
responseText += `Duration: ${result.durationMs}ms
|
|
883
|
+
`;
|
|
884
|
+
if (result.status === "ok" && result.output) {
|
|
885
|
+
const outputPreview = result.output.length > 500 ? `${result.output.slice(0, 500)}... (truncated)` : result.output;
|
|
886
|
+
responseText += `
|
|
887
|
+
Output:
|
|
888
|
+
${outputPreview}`;
|
|
1226
889
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
*/
|
|
1231
|
-
async emitCronEvent(eventName, job, result) {
|
|
1232
|
-
const eventData = {
|
|
1233
|
-
jobId: job.id,
|
|
1234
|
-
jobName: job.name,
|
|
1235
|
-
schedule: job.schedule
|
|
1236
|
-
};
|
|
1237
|
-
if (result) {
|
|
1238
|
-
eventData.result = {
|
|
1239
|
-
status: result.status,
|
|
1240
|
-
durationMs: result.durationMs,
|
|
1241
|
-
output: result.output,
|
|
1242
|
-
error: result.error
|
|
1243
|
-
};
|
|
890
|
+
if (result.error) {
|
|
891
|
+
responseText += `
|
|
892
|
+
Error: ${result.error}`;
|
|
1244
893
|
}
|
|
1245
|
-
await
|
|
1246
|
-
|
|
1247
|
-
source: `cron:${job.id}`,
|
|
1248
|
-
...eventData
|
|
894
|
+
await callback?.({
|
|
895
|
+
text: responseText
|
|
1249
896
|
});
|
|
1250
|
-
}
|
|
1251
|
-
// ============================================================================
|
|
1252
|
-
// STATUS AND DIAGNOSTICS
|
|
1253
|
-
// ============================================================================
|
|
1254
|
-
/**
|
|
1255
|
-
* Gets the service status
|
|
1256
|
-
*/
|
|
1257
|
-
async getStatus() {
|
|
1258
897
|
return {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
898
|
+
success: result.status === "ok",
|
|
899
|
+
data: {
|
|
900
|
+
jobId,
|
|
901
|
+
jobName,
|
|
902
|
+
result
|
|
903
|
+
}
|
|
1263
904
|
};
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
if (timeParts) {
|
|
1301
|
-
hours = parseInt(timeParts[1], 10);
|
|
1302
|
-
if (timeParts[2]) {
|
|
1303
|
-
minutes = parseInt(timeParts[2], 10);
|
|
1304
|
-
}
|
|
1305
|
-
if (timeParts[3]) {
|
|
1306
|
-
if (timeParts[3].toLowerCase() === "pm" && hours !== 12) {
|
|
1307
|
-
hours += 12;
|
|
1308
|
-
} else if (timeParts[3].toLowerCase() === "am" && hours === 12) {
|
|
1309
|
-
hours = 0;
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
if (normalized.includes("daily") || normalized.includes("every day")) {
|
|
1313
|
-
result.schedule = {
|
|
1314
|
-
kind: "cron",
|
|
1315
|
-
expr: `${minutes} ${hours} * * *`
|
|
1316
|
-
};
|
|
1317
|
-
} else if (normalized.includes("weekday") || normalized.includes("monday to friday")) {
|
|
1318
|
-
result.schedule = {
|
|
1319
|
-
kind: "cron",
|
|
1320
|
-
expr: `${minutes} ${hours} * * 1-5`
|
|
1321
|
-
};
|
|
1322
|
-
} else if (normalized.includes("weekend")) {
|
|
1323
|
-
result.schedule = {
|
|
1324
|
-
kind: "cron",
|
|
1325
|
-
expr: `${minutes} ${hours} * * 0,6`
|
|
1326
|
-
};
|
|
1327
|
-
} else {
|
|
1328
|
-
result.schedule = {
|
|
1329
|
-
kind: "cron",
|
|
1330
|
-
expr: `${minutes} ${hours} * * *`
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
const inMatch = /in\s+(\d+\s*(?:second|minute|hour|day)s?)/i.exec(text);
|
|
1336
|
-
if (inMatch && !result.schedule) {
|
|
1337
|
-
result.schedule = parseScheduleDescription(`in ${inMatch[1]}`);
|
|
1338
|
-
}
|
|
1339
|
-
const toMatch = /(?:to|that)\s+(.+?)(?:\s+every|\s+at|\s+in\s+\d|$)/i.exec(text);
|
|
1340
|
-
if (toMatch) {
|
|
1341
|
-
result.prompt = toMatch[1].trim();
|
|
1342
|
-
result.name = toMatch[1].slice(0, 50).trim();
|
|
1343
|
-
}
|
|
1344
|
-
const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
|
|
1345
|
-
if (nameMatch) {
|
|
1346
|
-
result.name = nameMatch[1].trim();
|
|
1347
|
-
}
|
|
1348
|
-
if (!result.name && result.schedule) {
|
|
1349
|
-
const scheduleDesc = formatSchedule(result.schedule);
|
|
1350
|
-
result.name = `Cron job (${scheduleDesc})`;
|
|
1351
|
-
}
|
|
1352
|
-
return result;
|
|
1353
|
-
}
|
|
1354
|
-
function formatJobResponse(job) {
|
|
1355
|
-
const scheduleStr = formatSchedule(job.schedule);
|
|
1356
|
-
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
|
|
1357
|
-
return `Created cron job "${job.name}"
|
|
1358
|
-
- ID: ${job.id}
|
|
1359
|
-
- Schedule: ${scheduleStr}
|
|
1360
|
-
- Status: ${job.enabled ? "enabled" : "disabled"}
|
|
1361
|
-
- Next run: ${nextRun}`;
|
|
1362
|
-
}
|
|
1363
|
-
var createCronAction = {
|
|
1364
|
-
name: CronActions.CREATE_CRON,
|
|
1365
|
-
similes: [
|
|
1366
|
-
"SCHEDULE_CRON",
|
|
1367
|
-
"ADD_CRON",
|
|
1368
|
-
"NEW_CRON",
|
|
1369
|
-
"CREATE_SCHEDULED_JOB",
|
|
1370
|
-
"SET_UP_CRON",
|
|
1371
|
-
"SCHEDULE_JOB",
|
|
1372
|
-
"CREATE_RECURRING_JOB"
|
|
1373
|
-
],
|
|
1374
|
-
description: "Creates a new cron job that runs on a schedule. Supports interval-based schedules (every X minutes), cron expressions, and one-time schedules.",
|
|
1375
|
-
validate: async (_runtime, message) => {
|
|
1376
|
-
const text = message.content?.text?.toLowerCase() ?? "";
|
|
1377
|
-
const hasScheduleKeyword = text.includes("cron") || text.includes("schedule") || text.includes("every ") || text.includes("recurring") || text.includes("repeat") || text.includes("daily") || text.includes("hourly") || text.includes("weekly");
|
|
1378
|
-
const hasCreateIntent = text.includes("create") || text.includes("add") || text.includes("set up") || text.includes("schedule") || text.includes("make");
|
|
1379
|
-
return hasScheduleKeyword && hasCreateIntent;
|
|
1380
|
-
},
|
|
1381
|
-
handler: async (runtime, message, _state, options, callback) => {
|
|
1382
|
-
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
1383
|
-
if (!cronService) {
|
|
1384
|
-
await callback?.({
|
|
1385
|
-
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
1386
|
-
});
|
|
1387
|
-
return { success: false, error: "Cron service not available" };
|
|
1388
|
-
}
|
|
1389
|
-
const text = message.content?.text ?? "";
|
|
1390
|
-
if (options?.jobInput && typeof options.jobInput === "object") {
|
|
1391
|
-
const input = options.jobInput;
|
|
1392
|
-
const scheduleError = validateSchedule(input.schedule, DEFAULT_CRON_CONFIG);
|
|
1393
|
-
if (scheduleError) {
|
|
1394
|
-
await callback?.({
|
|
1395
|
-
text: `Invalid schedule: ${scheduleError}`
|
|
1396
|
-
});
|
|
1397
|
-
return { success: false, error: scheduleError };
|
|
1398
|
-
}
|
|
1399
|
-
const job2 = await cronService.createJob(input);
|
|
1400
|
-
await callback?.({
|
|
1401
|
-
text: formatJobResponse(job2)
|
|
1402
|
-
});
|
|
1403
|
-
return { success: true, data: { jobId: job2.id, job: job2 } };
|
|
1404
|
-
}
|
|
1405
|
-
const parsed = parseNaturalLanguageRequest(text);
|
|
1406
|
-
if (!parsed.schedule) {
|
|
1407
|
-
await callback?.({
|
|
1408
|
-
text: `I couldn't understand the schedule. Please specify when the job should run, for example:
|
|
1409
|
-
- "every 5 minutes"
|
|
1410
|
-
- "every hour"
|
|
1411
|
-
- "daily at 9am"
|
|
1412
|
-
- "every weekday at 8:30am"`
|
|
1413
|
-
});
|
|
1414
|
-
return { success: false, error: "Could not parse schedule" };
|
|
1415
|
-
}
|
|
1416
|
-
const jobInput = {
|
|
1417
|
-
name: parsed.name || "Unnamed cron job",
|
|
1418
|
-
description: parsed.description,
|
|
1419
|
-
enabled: true,
|
|
1420
|
-
schedule: parsed.schedule,
|
|
1421
|
-
payload: {
|
|
1422
|
-
kind: "prompt",
|
|
1423
|
-
text: parsed.prompt || "Run scheduled task"
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
if (parsed.schedule.kind === "at") {
|
|
1427
|
-
jobInput.deleteAfterRun = true;
|
|
1428
|
-
}
|
|
1429
|
-
const job = await cronService.createJob(jobInput);
|
|
1430
|
-
await callback?.({
|
|
1431
|
-
text: formatJobResponse(job)
|
|
1432
|
-
});
|
|
1433
|
-
return {
|
|
1434
|
-
success: true,
|
|
1435
|
-
data: {
|
|
1436
|
-
jobId: job.id,
|
|
1437
|
-
job
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1440
|
-
},
|
|
1441
|
-
examples: [
|
|
1442
|
-
[
|
|
1443
|
-
{
|
|
1444
|
-
name: "{{user1}}",
|
|
1445
|
-
content: { text: "Create a cron job to check the news every hour" }
|
|
1446
|
-
},
|
|
1447
|
-
{
|
|
1448
|
-
name: "{{agentName}}",
|
|
1449
|
-
content: {
|
|
1450
|
-
text: 'Created cron job "check the news"\n- ID: abc-123\n- Schedule: every 1 hour\n- Status: enabled\n- Next run: in 1 hour'
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
],
|
|
1454
|
-
[
|
|
1455
|
-
{
|
|
1456
|
-
name: "{{user1}}",
|
|
1457
|
-
content: { text: "Schedule a daily reminder at 9am to review my goals" }
|
|
1458
|
-
},
|
|
1459
|
-
{
|
|
1460
|
-
name: "{{agentName}}",
|
|
1461
|
-
content: {
|
|
1462
|
-
text: 'Created cron job "review my goals"\n- ID: def-456\n- Schedule: cron: 0 9 * * *\n- Status: enabled\n- Next run: tomorrow at 9:00 AM'
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
],
|
|
1466
|
-
[
|
|
1467
|
-
{
|
|
1468
|
-
name: "{{user1}}",
|
|
1469
|
-
content: { text: "Set up a recurring job every 5 minutes to check server status" }
|
|
1470
|
-
},
|
|
1471
|
-
{
|
|
1472
|
-
name: "{{agentName}}",
|
|
1473
|
-
content: {
|
|
1474
|
-
text: 'Created cron job "check server status"\n- ID: ghi-789\n- Schedule: every 5 minutes\n- Status: enabled\n- Next run: in 5 minutes'
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
]
|
|
1478
|
-
]
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
// src/actions/update-cron.ts
|
|
1482
|
-
function extractJobIdentifier(text) {
|
|
1483
|
-
const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(text);
|
|
1484
|
-
if (idMatch) {
|
|
1485
|
-
return { id: idMatch[1] };
|
|
905
|
+
},
|
|
906
|
+
examples: [
|
|
907
|
+
[
|
|
908
|
+
{
|
|
909
|
+
name: "{{user1}}",
|
|
910
|
+
content: { text: "Run the cron job called daily-check now" }
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "{{agentName}}",
|
|
914
|
+
content: {
|
|
915
|
+
text: 'Ran cron job "daily-check" (abc-123)\nStatus: ok\nDuration: 1250ms\n\nOutput:\nDaily check completed successfully. All systems operational.'
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
],
|
|
919
|
+
[
|
|
920
|
+
{
|
|
921
|
+
name: "{{user1}}",
|
|
922
|
+
content: { text: "Execute cron abc-123-def-456" }
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
name: "{{agentName}}",
|
|
926
|
+
content: {
|
|
927
|
+
text: 'Ran cron job "status-checker" (abc-123-def-456)\nStatus: ok\nDuration: 850ms'
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
]
|
|
931
|
+
]
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// src/actions/update-cron.ts
|
|
935
|
+
function extractJobIdentifier3(text) {
|
|
936
|
+
const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(
|
|
937
|
+
text
|
|
938
|
+
);
|
|
939
|
+
if (idMatch) {
|
|
940
|
+
return { id: idMatch[1] };
|
|
1486
941
|
}
|
|
1487
942
|
const quotedMatch = /["']([^"']+)["']/i.exec(text);
|
|
1488
943
|
if (quotedMatch) {
|
|
@@ -1502,7 +957,9 @@ function parseUpdateIntent(text) {
|
|
|
1502
957
|
} else if (normalized.includes("disable")) {
|
|
1503
958
|
patch.enabled = false;
|
|
1504
959
|
}
|
|
1505
|
-
const everyMatch = /every\s+(\d+\s*(?:second|minute|hour|day|week)s?)/i.exec(
|
|
960
|
+
const everyMatch = /every\s+(\d+\s*(?:second|minute|hour|day|week)s?)/i.exec(
|
|
961
|
+
text
|
|
962
|
+
);
|
|
1506
963
|
if (everyMatch) {
|
|
1507
964
|
const schedule = parseScheduleDescription(`every ${everyMatch[1]}`);
|
|
1508
965
|
if (schedule) {
|
|
@@ -1545,13 +1002,13 @@ var updateCronAction = {
|
|
|
1545
1002
|
let jobId = options?.jobId;
|
|
1546
1003
|
let patch = options?.patch || {};
|
|
1547
1004
|
if (!jobId) {
|
|
1548
|
-
const identifier =
|
|
1005
|
+
const identifier = extractJobIdentifier3(text);
|
|
1549
1006
|
if (identifier.id) {
|
|
1550
1007
|
jobId = identifier.id;
|
|
1551
1008
|
} else if (identifier.name) {
|
|
1552
1009
|
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
1553
1010
|
const job = jobs.find(
|
|
1554
|
-
(j) => j.name.toLowerCase() === identifier.name
|
|
1011
|
+
(j) => j.name.toLowerCase() === identifier.name?.toLowerCase()
|
|
1555
1012
|
);
|
|
1556
1013
|
if (!job) {
|
|
1557
1014
|
await callback?.({
|
|
@@ -1631,682 +1088,1289 @@ Next run: ${nextRun}`
|
|
|
1631
1088
|
]
|
|
1632
1089
|
};
|
|
1633
1090
|
|
|
1634
|
-
// src/
|
|
1635
|
-
function
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1091
|
+
// src/providers/cron-context.ts
|
|
1092
|
+
function formatJobForContext(job) {
|
|
1093
|
+
const scheduleStr = formatSchedule(job.schedule);
|
|
1094
|
+
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "not scheduled";
|
|
1095
|
+
let line = `- ${job.name} (${scheduleStr})`;
|
|
1096
|
+
if (!job.enabled) {
|
|
1097
|
+
line += " [disabled]";
|
|
1098
|
+
} else {
|
|
1099
|
+
line += ` - next: ${nextRun}`;
|
|
1639
1100
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1101
|
+
if (job.state.lastStatus === "error") {
|
|
1102
|
+
line += " [last run failed]";
|
|
1103
|
+
}
|
|
1104
|
+
return line;
|
|
1105
|
+
}
|
|
1106
|
+
var cronContextProvider = {
|
|
1107
|
+
name: "cronContext",
|
|
1108
|
+
description: "Provides information about scheduled cron jobs",
|
|
1109
|
+
dynamic: true,
|
|
1110
|
+
position: 50,
|
|
1111
|
+
get: async (runtime, _message, _state) => {
|
|
1112
|
+
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
1113
|
+
if (!cronService) {
|
|
1114
|
+
return {
|
|
1115
|
+
text: "",
|
|
1116
|
+
values: {
|
|
1117
|
+
hasCronService: false,
|
|
1118
|
+
cronJobCount: 0
|
|
1119
|
+
},
|
|
1120
|
+
data: {
|
|
1121
|
+
available: false
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
1126
|
+
const enabledJobs = jobs.filter((job) => job.enabled);
|
|
1127
|
+
const disabledJobs = jobs.filter((job) => !job.enabled);
|
|
1128
|
+
const nowMs = Date.now();
|
|
1129
|
+
const oneHourFromNow = nowMs + 36e5;
|
|
1130
|
+
const upcomingJobs = enabledJobs.filter(
|
|
1131
|
+
(job) => job.state.nextRunAtMs && job.state.nextRunAtMs <= oneHourFromNow
|
|
1132
|
+
);
|
|
1133
|
+
const recentJobs = jobs.filter(
|
|
1134
|
+
(job) => job.state.lastRunAtMs && job.state.lastRunAtMs >= nowMs - 36e5
|
|
1135
|
+
);
|
|
1136
|
+
const failedJobs = jobs.filter((job) => job.state.lastStatus === "error");
|
|
1137
|
+
const lines = [];
|
|
1138
|
+
if (jobs.length === 0) {
|
|
1139
|
+
lines.push("No cron jobs are scheduled.");
|
|
1140
|
+
} else {
|
|
1141
|
+
lines.push(
|
|
1142
|
+
`Scheduled Jobs (${enabledJobs.length} active, ${disabledJobs.length} disabled):`
|
|
1143
|
+
);
|
|
1144
|
+
if (upcomingJobs.length > 0) {
|
|
1145
|
+
lines.push("\nUpcoming (next hour):");
|
|
1146
|
+
for (const job of upcomingJobs.slice(0, 5)) {
|
|
1147
|
+
lines.push(formatJobForContext(job));
|
|
1148
|
+
}
|
|
1149
|
+
if (upcomingJobs.length > 5) {
|
|
1150
|
+
lines.push(` ... and ${upcomingJobs.length - 5} more`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (failedJobs.length > 0) {
|
|
1154
|
+
lines.push("\nRecently failed:");
|
|
1155
|
+
for (const job of failedJobs.slice(0, 3)) {
|
|
1156
|
+
lines.push(
|
|
1157
|
+
`- ${job.name}: ${job.state.lastError || "unknown error"}`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (enabledJobs.length <= 10 && enabledJobs.length > 0) {
|
|
1162
|
+
lines.push("\nAll active jobs:");
|
|
1163
|
+
for (const job of enabledJobs) {
|
|
1164
|
+
lines.push(formatJobForContext(job));
|
|
1165
|
+
}
|
|
1166
|
+
} else if (enabledJobs.length > 10) {
|
|
1167
|
+
lines.push(
|
|
1168
|
+
`
|
|
1169
|
+
${enabledJobs.length} active jobs total. Use "list crons" to see all.`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
text: lines.join("\n"),
|
|
1175
|
+
values: {
|
|
1176
|
+
hasCronService: true,
|
|
1177
|
+
cronJobCount: jobs.length,
|
|
1178
|
+
enabledJobCount: enabledJobs.length,
|
|
1179
|
+
disabledJobCount: disabledJobs.length,
|
|
1180
|
+
upcomingJobCount: upcomingJobs.length,
|
|
1181
|
+
failedJobCount: failedJobs.length
|
|
1182
|
+
},
|
|
1183
|
+
data: {
|
|
1184
|
+
available: true,
|
|
1185
|
+
jobs: jobs.map((job) => ({
|
|
1186
|
+
id: job.id,
|
|
1187
|
+
name: job.name,
|
|
1188
|
+
enabled: job.enabled,
|
|
1189
|
+
schedule: job.schedule,
|
|
1190
|
+
nextRunAtMs: job.state.nextRunAtMs,
|
|
1191
|
+
lastStatus: job.state.lastStatus
|
|
1192
|
+
})),
|
|
1193
|
+
upcoming: upcomingJobs.map((job) => job.id),
|
|
1194
|
+
failed: failedJobs.map((job) => job.id),
|
|
1195
|
+
recent: recentJobs.map((job) => job.id)
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/routes/index.ts
|
|
1202
|
+
function getCronService(runtime) {
|
|
1203
|
+
const svc = runtime.getService(CRON_SERVICE_TYPE);
|
|
1204
|
+
if (!svc) {
|
|
1205
|
+
throw new Error("CronService not available");
|
|
1206
|
+
}
|
|
1207
|
+
return svc;
|
|
1208
|
+
}
|
|
1209
|
+
async function handleCronStatus(_req, res, runtime) {
|
|
1210
|
+
const svc = getCronService(runtime);
|
|
1211
|
+
const status = await svc.getStatus();
|
|
1212
|
+
res.json({
|
|
1213
|
+
enabled: status.initialized,
|
|
1214
|
+
jobs: status.jobCount,
|
|
1215
|
+
tracked: status.trackedJobCount,
|
|
1216
|
+
config: status.config
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
async function handleCronList(req, res, runtime) {
|
|
1220
|
+
const includeDisabled = req.body?.includeDisabled === true;
|
|
1221
|
+
const svc = getCronService(runtime);
|
|
1222
|
+
const jobs = await svc.listJobs({ includeDisabled });
|
|
1223
|
+
res.json({ jobs });
|
|
1224
|
+
}
|
|
1225
|
+
async function handleCronAdd(req, res, runtime) {
|
|
1226
|
+
const body = req.body ?? {};
|
|
1227
|
+
const normalized = normalizeCronJobCreate(body);
|
|
1228
|
+
if (!normalized) {
|
|
1229
|
+
res.status(400).json({ error: "Invalid job input" });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const svc = getCronService(runtime);
|
|
1233
|
+
const job = await svc.createJob(
|
|
1234
|
+
normalized
|
|
1235
|
+
);
|
|
1236
|
+
res.json({ job });
|
|
1237
|
+
}
|
|
1238
|
+
async function handleCronUpdate(req, res, runtime) {
|
|
1239
|
+
const body = req.body ?? {};
|
|
1240
|
+
const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
|
|
1241
|
+
if (!jobId) {
|
|
1242
|
+
res.status(400).json({ error: "Missing jobId" });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const patch = normalizeCronJobPatch(body.patch ?? body);
|
|
1246
|
+
if (!patch) {
|
|
1247
|
+
res.status(400).json({ error: "Invalid patch" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const svc = getCronService(runtime);
|
|
1251
|
+
const job = await svc.updateJob(
|
|
1252
|
+
jobId,
|
|
1253
|
+
patch
|
|
1254
|
+
);
|
|
1255
|
+
res.json({ job });
|
|
1256
|
+
}
|
|
1257
|
+
async function handleCronRemove(req, res, runtime) {
|
|
1258
|
+
const body = req.body ?? {};
|
|
1259
|
+
const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
|
|
1260
|
+
if (!jobId) {
|
|
1261
|
+
res.status(400).json({ error: "Missing jobId" });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const svc = getCronService(runtime);
|
|
1265
|
+
const deleted = await svc.deleteJob(jobId);
|
|
1266
|
+
res.json({ deleted });
|
|
1267
|
+
}
|
|
1268
|
+
async function handleCronRun(req, res, runtime) {
|
|
1269
|
+
const body = req.body ?? {};
|
|
1270
|
+
const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
|
|
1271
|
+
if (!jobId) {
|
|
1272
|
+
res.status(400).json({ error: "Missing jobId" });
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const mode = body.mode === "due" ? "due" : "force";
|
|
1276
|
+
const svc = getCronService(runtime);
|
|
1277
|
+
const result = await svc.runJob(jobId, mode);
|
|
1278
|
+
res.json(result);
|
|
1279
|
+
}
|
|
1280
|
+
async function handleCronRuns(req, res, _runtime) {
|
|
1281
|
+
const body = req.body ?? {};
|
|
1282
|
+
const jobId = typeof body.id === "string" ? body.id : "";
|
|
1283
|
+
const limit = typeof body.limit === "number" ? body.limit : 50;
|
|
1284
|
+
if (!jobId) {
|
|
1285
|
+
res.status(400).json({ error: "Missing id" });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const storePath = resolveCronStorePath();
|
|
1289
|
+
if (!storePath) {
|
|
1290
|
+
res.json({ entries: [] });
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const logPath = resolveCronRunLogPath({ storePath, jobId });
|
|
1294
|
+
const entries = await readCronRunLogEntries(logPath, { limit, jobId });
|
|
1295
|
+
res.json({ entries });
|
|
1296
|
+
}
|
|
1297
|
+
var cronRoutes = [
|
|
1298
|
+
{ type: "POST", path: "/api/cron/status", handler: handleCronStatus },
|
|
1299
|
+
{ type: "POST", path: "/api/cron/list", handler: handleCronList },
|
|
1300
|
+
{ type: "POST", path: "/api/cron/add", handler: handleCronAdd },
|
|
1301
|
+
{ type: "POST", path: "/api/cron/update", handler: handleCronUpdate },
|
|
1302
|
+
{ type: "POST", path: "/api/cron/remove", handler: handleCronRemove },
|
|
1303
|
+
{ type: "POST", path: "/api/cron/run", handler: handleCronRun },
|
|
1304
|
+
{ type: "POST", path: "/api/cron/runs", handler: handleCronRuns }
|
|
1305
|
+
];
|
|
1306
|
+
|
|
1307
|
+
// src/services/cron-service.ts
|
|
1308
|
+
import { logger, Service } from "@elizaos/core";
|
|
1309
|
+
import { v4 as uuidv43 } from "uuid";
|
|
1310
|
+
|
|
1311
|
+
// src/executor/job-executor.ts
|
|
1312
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1313
|
+
function createTimeoutController(timeoutMs) {
|
|
1314
|
+
const controller = new AbortController();
|
|
1315
|
+
const timer = setTimeout(() => {
|
|
1316
|
+
controller.abort(new Error("Job execution timeout"));
|
|
1317
|
+
}, timeoutMs);
|
|
1318
|
+
return {
|
|
1319
|
+
controller,
|
|
1320
|
+
cleanup: () => clearTimeout(timer)
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
async function withEnforcedTimeout(operation, timeoutMs, signal) {
|
|
1324
|
+
if (signal.aborted) {
|
|
1325
|
+
throw new Error("Job execution timeout");
|
|
1326
|
+
}
|
|
1327
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1328
|
+
const timeoutId = setTimeout(() => {
|
|
1329
|
+
reject(new Error("Job execution timeout"));
|
|
1330
|
+
}, timeoutMs);
|
|
1331
|
+
signal.addEventListener("abort", () => {
|
|
1332
|
+
clearTimeout(timeoutId);
|
|
1333
|
+
reject(new Error("Job execution timeout"));
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
return Promise.race([operation(), timeoutPromise]);
|
|
1337
|
+
}
|
|
1338
|
+
async function executePromptPayload(runtime, payload, context) {
|
|
1339
|
+
const cronContext = `[Cron Job: ${context.job.name}]${context.job.description ? ` - ${context.job.description}` : ""}`;
|
|
1340
|
+
const fullPrompt = `${cronContext}
|
|
1341
|
+
|
|
1342
|
+
${payload.text}`;
|
|
1343
|
+
const result = await runtime.useModel("TEXT_LARGE", {
|
|
1344
|
+
prompt: fullPrompt
|
|
1345
|
+
});
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
async function executeActionPayload(runtime, payload, context) {
|
|
1349
|
+
const actions = runtime.actions ?? [];
|
|
1350
|
+
const action = actions.find(
|
|
1351
|
+
(a) => a.name.toLowerCase() === payload.actionName.toLowerCase()
|
|
1352
|
+
);
|
|
1353
|
+
if (!action) {
|
|
1354
|
+
throw new Error(`Action not found: ${payload.actionName}`);
|
|
1355
|
+
}
|
|
1356
|
+
const roomId = payload.roomId || runtime.agentId;
|
|
1357
|
+
const memory = {
|
|
1358
|
+
id: uuidv4(),
|
|
1359
|
+
entityId: runtime.agentId,
|
|
1360
|
+
roomId,
|
|
1361
|
+
agentId: runtime.agentId,
|
|
1362
|
+
content: {
|
|
1363
|
+
text: `[Cron Job: ${context.job.name}] Executing action: ${payload.actionName}`,
|
|
1364
|
+
// Spread params into content for actions to access
|
|
1365
|
+
...payload.params
|
|
1366
|
+
},
|
|
1367
|
+
createdAt: Date.now()
|
|
1368
|
+
};
|
|
1369
|
+
const callbackResponses = [];
|
|
1370
|
+
const callback = async (response) => {
|
|
1371
|
+
if (response.text) {
|
|
1372
|
+
callbackResponses.push(response.text);
|
|
1373
|
+
}
|
|
1374
|
+
return [];
|
|
1375
|
+
};
|
|
1376
|
+
const isValid = await action.validate(runtime, memory, void 0);
|
|
1377
|
+
if (!isValid) {
|
|
1378
|
+
throw new Error(`Action validation failed: ${payload.actionName}`);
|
|
1379
|
+
}
|
|
1380
|
+
const handlerResult = await action.handler(
|
|
1381
|
+
runtime,
|
|
1382
|
+
memory,
|
|
1383
|
+
void 0,
|
|
1384
|
+
void 0,
|
|
1385
|
+
callback
|
|
1386
|
+
);
|
|
1387
|
+
const outputParts = [];
|
|
1388
|
+
if (callbackResponses.length > 0) {
|
|
1389
|
+
outputParts.push(...callbackResponses);
|
|
1390
|
+
}
|
|
1391
|
+
if (handlerResult !== void 0 && handlerResult !== null) {
|
|
1392
|
+
if (typeof handlerResult === "string") {
|
|
1393
|
+
outputParts.push(handlerResult);
|
|
1394
|
+
} else if (typeof handlerResult === "object") {
|
|
1395
|
+
const result = handlerResult;
|
|
1396
|
+
if (result.text && typeof result.text === "string") {
|
|
1397
|
+
outputParts.push(result.text);
|
|
1398
|
+
} else if (result.success !== void 0 || result.data !== void 0) {
|
|
1399
|
+
outputParts.push(JSON.stringify(handlerResult));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return outputParts.join("\n") || `Action ${payload.actionName} completed`;
|
|
1404
|
+
}
|
|
1405
|
+
async function executeEventPayload(runtime, payload, context) {
|
|
1406
|
+
const eventPayload = {
|
|
1407
|
+
runtime,
|
|
1408
|
+
source: `cron:${context.job.id}`,
|
|
1409
|
+
cronJob: {
|
|
1410
|
+
id: context.job.id,
|
|
1411
|
+
name: context.job.name
|
|
1412
|
+
},
|
|
1413
|
+
...payload.payload || {}
|
|
1414
|
+
};
|
|
1415
|
+
await runtime.emitEvent(payload.eventName, eventPayload);
|
|
1416
|
+
return `Event ${payload.eventName} emitted`;
|
|
1417
|
+
}
|
|
1418
|
+
async function executeJob(runtime, job, config) {
|
|
1419
|
+
const payloadRecord = job.payload;
|
|
1420
|
+
if (isOttoPayload(payloadRecord)) {
|
|
1421
|
+
return executeOttoJob(runtime, job, config);
|
|
1422
|
+
}
|
|
1423
|
+
const startedAtMs = Date.now();
|
|
1424
|
+
let timeoutMs = config.defaultTimeoutMs ?? DEFAULT_CRON_CONFIG.defaultTimeoutMs;
|
|
1425
|
+
if (job.payload.kind === "prompt" && job.payload.timeoutSeconds) {
|
|
1426
|
+
timeoutMs = job.payload.timeoutSeconds * 1e3;
|
|
1427
|
+
}
|
|
1428
|
+
const { controller, cleanup } = createTimeoutController(timeoutMs);
|
|
1429
|
+
const context = {
|
|
1430
|
+
job,
|
|
1431
|
+
startedAtMs,
|
|
1432
|
+
signal: controller.signal
|
|
1433
|
+
};
|
|
1434
|
+
let status = "ok";
|
|
1435
|
+
let output;
|
|
1436
|
+
let error;
|
|
1437
|
+
try {
|
|
1438
|
+
if (controller.signal.aborted) {
|
|
1439
|
+
throw new Error("Job execution timeout");
|
|
1440
|
+
}
|
|
1441
|
+
const executeOperation = async () => {
|
|
1442
|
+
switch (job.payload.kind) {
|
|
1443
|
+
case "prompt":
|
|
1444
|
+
return executePromptPayload(runtime, job.payload, context);
|
|
1445
|
+
case "action":
|
|
1446
|
+
return executeActionPayload(runtime, job.payload, context);
|
|
1447
|
+
case "event":
|
|
1448
|
+
return executeEventPayload(runtime, job.payload, context);
|
|
1449
|
+
default: {
|
|
1450
|
+
const _exhaustive = job.payload;
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
`Unknown payload kind: ${job.payload.kind}`
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
output = await withEnforcedTimeout(
|
|
1458
|
+
executeOperation,
|
|
1459
|
+
timeoutMs,
|
|
1460
|
+
controller.signal
|
|
1461
|
+
);
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
if (err instanceof Error) {
|
|
1464
|
+
if (err.message === "Job execution timeout" || controller.signal.aborted) {
|
|
1465
|
+
status = "timeout";
|
|
1466
|
+
error = "Execution timed out";
|
|
1467
|
+
} else {
|
|
1468
|
+
status = "error";
|
|
1469
|
+
error = err.message;
|
|
1470
|
+
}
|
|
1471
|
+
} else {
|
|
1472
|
+
status = "error";
|
|
1473
|
+
error = String(err);
|
|
1474
|
+
}
|
|
1475
|
+
} finally {
|
|
1476
|
+
cleanup();
|
|
1477
|
+
}
|
|
1478
|
+
const durationMs = Date.now() - startedAtMs;
|
|
1479
|
+
return {
|
|
1480
|
+
status,
|
|
1481
|
+
durationMs,
|
|
1482
|
+
output,
|
|
1483
|
+
error
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function validateJobExecutability(runtime, job) {
|
|
1487
|
+
const { payload } = job;
|
|
1488
|
+
const payloadRecord = payload;
|
|
1489
|
+
if (isOttoPayload(payloadRecord)) {
|
|
1490
|
+
if (payloadRecord.kind === "systemEvent") {
|
|
1491
|
+
const text = typeof payloadRecord.text === "string" ? payloadRecord.text.trim() : "";
|
|
1492
|
+
return text ? null : "systemEvent payload must have non-empty text";
|
|
1493
|
+
}
|
|
1494
|
+
if (payloadRecord.kind === "agentTurn") {
|
|
1495
|
+
const message = typeof payloadRecord.message === "string" ? payloadRecord.message.trim() : "";
|
|
1496
|
+
return message ? null : "agentTurn payload must have non-empty message";
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
switch (payload.kind) {
|
|
1501
|
+
case "prompt": {
|
|
1502
|
+
const text = payload.text?.trim();
|
|
1503
|
+
if (!text) {
|
|
1504
|
+
return "Prompt payload must have non-empty text";
|
|
1505
|
+
}
|
|
1506
|
+
if (typeof runtime.useModel !== "function") {
|
|
1507
|
+
return "Runtime does not support useModel for prompt execution";
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
case "event": {
|
|
1512
|
+
const eventName = payload.eventName?.trim();
|
|
1513
|
+
if (!eventName) {
|
|
1514
|
+
return "Event payload must have non-empty eventName";
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof runtime.emitEvent !== "function") {
|
|
1517
|
+
return "Runtime does not support emitEvent for event execution";
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
case "action": {
|
|
1522
|
+
const actionName = payload.actionName?.trim();
|
|
1523
|
+
if (!actionName) {
|
|
1524
|
+
return "Action payload must have non-empty actionName";
|
|
1525
|
+
}
|
|
1526
|
+
const actions = runtime.actions ?? [];
|
|
1527
|
+
const action = actions.find(
|
|
1528
|
+
(a) => a.name.toLowerCase() === actionName.toLowerCase()
|
|
1529
|
+
);
|
|
1530
|
+
return action ? null : `Action not found: ${payload.actionName}`;
|
|
1531
|
+
}
|
|
1532
|
+
default: {
|
|
1533
|
+
return `Unknown payload kind: ${payload.kind}`;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/scheduler/timer-manager.ts
|
|
1539
|
+
import { Cron as Cron2 } from "croner";
|
|
1540
|
+
var TimerManager = class {
|
|
1541
|
+
config;
|
|
1542
|
+
onJobDue;
|
|
1543
|
+
trackedJobs = /* @__PURE__ */ new Map();
|
|
1544
|
+
checkInterval = null;
|
|
1545
|
+
running = false;
|
|
1546
|
+
constructor(config, onJobDue) {
|
|
1547
|
+
this.config = config;
|
|
1548
|
+
this.onJobDue = onJobDue;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Starts the timer manager
|
|
1552
|
+
*/
|
|
1553
|
+
start() {
|
|
1554
|
+
if (this.running) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
this.running = true;
|
|
1558
|
+
this.startCheckInterval();
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Stops the timer manager and cleans up all timers
|
|
1562
|
+
*/
|
|
1563
|
+
stop() {
|
|
1564
|
+
this.running = false;
|
|
1565
|
+
this.stopCheckInterval();
|
|
1566
|
+
this.clearAllJobs();
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Adds or updates a job in the timer manager
|
|
1570
|
+
* @param job The job to track
|
|
1571
|
+
*/
|
|
1572
|
+
trackJob(job) {
|
|
1573
|
+
this.untrackJob(job.id);
|
|
1574
|
+
if (!job.enabled) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const tracked = {
|
|
1578
|
+
job,
|
|
1579
|
+
executing: false
|
|
1580
|
+
};
|
|
1581
|
+
if (job.schedule.kind === "cron") {
|
|
1582
|
+
const cronInstance = new Cron2(job.schedule.expr, {
|
|
1583
|
+
timezone: job.schedule.tz?.trim() || void 0,
|
|
1584
|
+
catch: true,
|
|
1585
|
+
paused: true
|
|
1586
|
+
// We manage firing manually via the check interval
|
|
1587
|
+
});
|
|
1588
|
+
tracked.cronInstance = cronInstance;
|
|
1589
|
+
}
|
|
1590
|
+
tracked.nextRunAtMs = job.state.nextRunAtMs ?? computeNextRunAtMs(job.schedule, Date.now());
|
|
1591
|
+
this.trackedJobs.set(job.id, tracked);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Removes a job from tracking
|
|
1595
|
+
* @param jobId The job ID to remove
|
|
1596
|
+
*/
|
|
1597
|
+
untrackJob(jobId) {
|
|
1598
|
+
const tracked = this.trackedJobs.get(jobId);
|
|
1599
|
+
if (tracked) {
|
|
1600
|
+
if (tracked.cronInstance) {
|
|
1601
|
+
tracked.cronInstance.stop();
|
|
1602
|
+
}
|
|
1603
|
+
this.trackedJobs.delete(jobId);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Marks a job as currently executing (to prevent overlapping executions)
|
|
1608
|
+
* @param jobId The job ID
|
|
1609
|
+
*/
|
|
1610
|
+
markExecuting(jobId) {
|
|
1611
|
+
const tracked = this.trackedJobs.get(jobId);
|
|
1612
|
+
if (tracked) {
|
|
1613
|
+
tracked.executing = true;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Marks a job as finished executing and recalculates next run
|
|
1618
|
+
* @param jobId The job ID
|
|
1619
|
+
* @param updatedJob The job with updated state (optional)
|
|
1620
|
+
*/
|
|
1621
|
+
markFinished(jobId, updatedJob) {
|
|
1622
|
+
const tracked = this.trackedJobs.get(jobId);
|
|
1623
|
+
if (tracked) {
|
|
1624
|
+
tracked.executing = false;
|
|
1625
|
+
if (updatedJob) {
|
|
1626
|
+
tracked.job = updatedJob;
|
|
1627
|
+
tracked.nextRunAtMs = computeNextRunAtMs(
|
|
1628
|
+
updatedJob.schedule,
|
|
1629
|
+
Date.now()
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Gets the next scheduled run time for a job
|
|
1636
|
+
* @param jobId The job ID
|
|
1637
|
+
* @returns Next run time in ms, or undefined
|
|
1638
|
+
*/
|
|
1639
|
+
getNextRunAtMs(jobId) {
|
|
1640
|
+
return this.trackedJobs.get(jobId)?.nextRunAtMs;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Gets all tracked job IDs
|
|
1644
|
+
*/
|
|
1645
|
+
getTrackedJobIds() {
|
|
1646
|
+
return Array.from(this.trackedJobs.keys());
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Gets the count of tracked jobs
|
|
1650
|
+
*/
|
|
1651
|
+
getTrackedJobCount() {
|
|
1652
|
+
return this.trackedJobs.size;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Checks if a specific job is currently executing
|
|
1656
|
+
*/
|
|
1657
|
+
isJobExecuting(jobId) {
|
|
1658
|
+
return this.trackedJobs.get(jobId)?.executing ?? false;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Forces an immediate check for due jobs
|
|
1662
|
+
*/
|
|
1663
|
+
checkNow() {
|
|
1664
|
+
if (this.running) {
|
|
1665
|
+
this.performCheck();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Starts the periodic check interval
|
|
1670
|
+
*/
|
|
1671
|
+
startCheckInterval() {
|
|
1672
|
+
if (this.checkInterval) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const intervalMs = this.config.timerCheckIntervalMs ?? DEFAULT_CRON_CONFIG.timerCheckIntervalMs;
|
|
1676
|
+
this.checkInterval = setInterval(() => {
|
|
1677
|
+
this.performCheck();
|
|
1678
|
+
}, intervalMs);
|
|
1679
|
+
if (this.checkInterval.unref) {
|
|
1680
|
+
this.checkInterval.unref();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Stops the periodic check interval
|
|
1685
|
+
*/
|
|
1686
|
+
stopCheckInterval() {
|
|
1687
|
+
if (this.checkInterval) {
|
|
1688
|
+
clearInterval(this.checkInterval);
|
|
1689
|
+
this.checkInterval = null;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Performs a check for all due jobs
|
|
1694
|
+
*/
|
|
1695
|
+
performCheck() {
|
|
1696
|
+
const nowMs = Date.now();
|
|
1697
|
+
const dueJobs = [];
|
|
1698
|
+
for (const [jobId, tracked] of this.trackedJobs) {
|
|
1699
|
+
if (tracked.executing) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (!tracked.job.enabled) {
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
if (isJobDue(tracked.nextRunAtMs, nowMs)) {
|
|
1706
|
+
dueJobs.push(jobId);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
for (const jobId of dueJobs) {
|
|
1710
|
+
const tracked = this.trackedJobs.get(jobId);
|
|
1711
|
+
if (tracked && !tracked.executing) {
|
|
1712
|
+
tracked.executing = true;
|
|
1713
|
+
this.onJobDue(jobId).catch((error) => {
|
|
1714
|
+
console.error(
|
|
1715
|
+
`[CronTimerManager] Error executing job ${jobId}:`,
|
|
1716
|
+
error
|
|
1717
|
+
);
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Clears all tracked jobs
|
|
1724
|
+
*/
|
|
1725
|
+
clearAllJobs() {
|
|
1726
|
+
for (const [, tracked] of this.trackedJobs) {
|
|
1727
|
+
if (tracked.cronInstance) {
|
|
1728
|
+
tracked.cronInstance.stop();
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
this.trackedJobs.clear();
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// src/storage/cron-storage.ts
|
|
1736
|
+
import { v4 as uuidv42 } from "uuid";
|
|
1737
|
+
var AsyncMutex = class {
|
|
1738
|
+
queue = [];
|
|
1739
|
+
locked = false;
|
|
1740
|
+
async acquire() {
|
|
1741
|
+
return new Promise((resolve) => {
|
|
1742
|
+
const tryAcquire = () => {
|
|
1743
|
+
if (!this.locked) {
|
|
1744
|
+
this.locked = true;
|
|
1745
|
+
resolve(() => this.release());
|
|
1746
|
+
} else {
|
|
1747
|
+
this.queue.push(tryAcquire);
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
tryAcquire();
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
release() {
|
|
1754
|
+
this.locked = false;
|
|
1755
|
+
const next = this.queue.shift();
|
|
1756
|
+
if (next) {
|
|
1757
|
+
next();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
var indexMutexes = /* @__PURE__ */ new Map();
|
|
1762
|
+
function getIndexMutex(agentId) {
|
|
1763
|
+
let mutex = indexMutexes.get(agentId);
|
|
1764
|
+
if (!mutex) {
|
|
1765
|
+
mutex = new AsyncMutex();
|
|
1766
|
+
indexMutexes.set(agentId, mutex);
|
|
1767
|
+
}
|
|
1768
|
+
return mutex;
|
|
1769
|
+
}
|
|
1770
|
+
function getJobComponentType(jobId) {
|
|
1771
|
+
return `${CRON_JOB_COMPONENT_PREFIX}:${jobId}`;
|
|
1772
|
+
}
|
|
1773
|
+
async function getJobIndex(runtime) {
|
|
1774
|
+
const component = await runtime.getComponent(
|
|
1775
|
+
runtime.agentId,
|
|
1776
|
+
CRON_JOB_INDEX_COMPONENT
|
|
1777
|
+
);
|
|
1778
|
+
if (!component) {
|
|
1779
|
+
return { jobIds: [] };
|
|
1780
|
+
}
|
|
1781
|
+
return component.data;
|
|
1782
|
+
}
|
|
1783
|
+
async function saveJobIndex(runtime, index) {
|
|
1784
|
+
const existing = await runtime.getComponent(
|
|
1785
|
+
runtime.agentId,
|
|
1786
|
+
CRON_JOB_INDEX_COMPONENT
|
|
1787
|
+
);
|
|
1788
|
+
const component = {
|
|
1789
|
+
id: existing?.id || uuidv42(),
|
|
1790
|
+
entityId: runtime.agentId,
|
|
1791
|
+
agentId: runtime.agentId,
|
|
1792
|
+
roomId: runtime.agentId,
|
|
1793
|
+
// Use agentId as room for agent-scoped data
|
|
1794
|
+
worldId: existing?.worldId || uuidv42(),
|
|
1795
|
+
sourceEntityId: runtime.agentId,
|
|
1796
|
+
type: CRON_JOB_INDEX_COMPONENT,
|
|
1797
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
1798
|
+
data: index
|
|
1799
|
+
};
|
|
1800
|
+
if (existing) {
|
|
1801
|
+
await runtime.updateComponent(component);
|
|
1802
|
+
} else {
|
|
1803
|
+
await runtime.createComponent(component);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function addToIndex(runtime, jobId) {
|
|
1807
|
+
const mutex = getIndexMutex(runtime.agentId);
|
|
1808
|
+
const release = await mutex.acquire();
|
|
1809
|
+
try {
|
|
1810
|
+
const index = await getJobIndex(runtime);
|
|
1811
|
+
if (!index.jobIds.includes(jobId)) {
|
|
1812
|
+
index.jobIds.push(jobId);
|
|
1813
|
+
await saveJobIndex(runtime, index);
|
|
1814
|
+
}
|
|
1815
|
+
} finally {
|
|
1816
|
+
release();
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
async function removeFromIndex(runtime, jobId) {
|
|
1820
|
+
const mutex = getIndexMutex(runtime.agentId);
|
|
1821
|
+
const release = await mutex.acquire();
|
|
1822
|
+
try {
|
|
1823
|
+
const index = await getJobIndex(runtime);
|
|
1824
|
+
const filtered = index.jobIds.filter((id) => id !== jobId);
|
|
1825
|
+
if (filtered.length !== index.jobIds.length) {
|
|
1826
|
+
index.jobIds = filtered;
|
|
1827
|
+
await saveJobIndex(runtime, index);
|
|
1828
|
+
}
|
|
1829
|
+
} finally {
|
|
1830
|
+
release();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function validateJobData(data) {
|
|
1834
|
+
if (!data || typeof data !== "object") {
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
const obj = data;
|
|
1838
|
+
if (typeof obj.id !== "string" || !obj.id) {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
if (typeof obj.name !== "string" || !obj.name) {
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
if (typeof obj.createdAtMs !== "number") {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
if (typeof obj.updatedAtMs !== "number") {
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
if (!obj.schedule || typeof obj.schedule !== "object") {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
const schedule = obj.schedule;
|
|
1854
|
+
const validScheduleKinds = ["at", "every", "cron"];
|
|
1855
|
+
if (!validScheduleKinds.includes(schedule.kind)) {
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
if (!obj.payload || typeof obj.payload !== "object") {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const payload = obj.payload;
|
|
1862
|
+
const validPayloadKinds = ["prompt", "action", "event"];
|
|
1863
|
+
if (!validPayloadKinds.includes(payload.kind)) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
if (!obj.state || typeof obj.state !== "object") {
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
const state = obj.state;
|
|
1870
|
+
if (typeof state.runCount !== "number" || typeof state.errorCount !== "number") {
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
const job = {
|
|
1874
|
+
...data,
|
|
1875
|
+
enabled: typeof obj.enabled === "boolean" ? obj.enabled : true,
|
|
1876
|
+
deleteAfterRun: typeof obj.deleteAfterRun === "boolean" ? obj.deleteAfterRun : false
|
|
1877
|
+
};
|
|
1878
|
+
return job;
|
|
1879
|
+
}
|
|
1880
|
+
function matchesFilter(job, filter) {
|
|
1881
|
+
if (!filter) return true;
|
|
1882
|
+
if (filter.enabled !== void 0) {
|
|
1883
|
+
if (job.enabled !== filter.enabled) return false;
|
|
1884
|
+
} else if (!filter.includeDisabled && !job.enabled) {
|
|
1885
|
+
return false;
|
|
1643
1886
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
return { name: namedMatch[1] };
|
|
1887
|
+
if (filter.tags?.length) {
|
|
1888
|
+
if (!job.tags?.some((tag) => filter.tags?.includes(tag))) return false;
|
|
1647
1889
|
}
|
|
1648
|
-
return
|
|
1890
|
+
return true;
|
|
1649
1891
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
validate: async (_runtime, message) => {
|
|
1661
|
-
const text = message.content?.text?.toLowerCase() ?? "";
|
|
1662
|
-
const hasDeleteKeyword = text.includes("delete") || text.includes("remove") || text.includes("cancel") || text.includes("stop") && !text.includes("stop running");
|
|
1663
|
-
const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
|
|
1664
|
-
return hasDeleteKeyword && hasCronKeyword;
|
|
1665
|
-
},
|
|
1666
|
-
handler: async (runtime, message, _state, options, callback) => {
|
|
1667
|
-
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
1668
|
-
if (!cronService) {
|
|
1669
|
-
await callback?.({
|
|
1670
|
-
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
1671
|
-
});
|
|
1672
|
-
return { success: false, error: "Cron service not available" };
|
|
1673
|
-
}
|
|
1674
|
-
const text = message.content?.text ?? "";
|
|
1675
|
-
let jobId = options?.jobId;
|
|
1676
|
-
let jobName;
|
|
1677
|
-
if (!jobId) {
|
|
1678
|
-
const identifier = extractJobIdentifier2(text);
|
|
1679
|
-
if (identifier.id) {
|
|
1680
|
-
jobId = identifier.id;
|
|
1681
|
-
} else if (identifier.name) {
|
|
1682
|
-
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
1683
|
-
const job = jobs.find(
|
|
1684
|
-
(j) => j.name.toLowerCase() === identifier.name.toLowerCase()
|
|
1685
|
-
);
|
|
1686
|
-
if (!job) {
|
|
1687
|
-
await callback?.({
|
|
1688
|
-
text: `No cron job found with name: ${identifier.name}`
|
|
1689
|
-
});
|
|
1690
|
-
return { success: false, error: "Job not found" };
|
|
1691
|
-
}
|
|
1692
|
-
jobId = job.id;
|
|
1693
|
-
jobName = job.name;
|
|
1892
|
+
function getCronStorage(runtime) {
|
|
1893
|
+
return {
|
|
1894
|
+
async getJob(jobId) {
|
|
1895
|
+
const componentType = getJobComponentType(jobId);
|
|
1896
|
+
const component = await runtime.getComponent(
|
|
1897
|
+
runtime.agentId,
|
|
1898
|
+
componentType
|
|
1899
|
+
);
|
|
1900
|
+
if (!component) {
|
|
1901
|
+
return null;
|
|
1694
1902
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
});
|
|
1700
|
-
return { success: false, error: "No job identifier provided" };
|
|
1701
|
-
}
|
|
1702
|
-
if (!jobName) {
|
|
1703
|
-
const job = await cronService.getJob(jobId);
|
|
1704
|
-
if (job) {
|
|
1705
|
-
jobName = job.name;
|
|
1903
|
+
const validatedJob = validateJobData(component.data);
|
|
1904
|
+
if (!validatedJob) {
|
|
1905
|
+
console.warn(`[cron-storage] Invalid job data for ${jobId}, skipping`);
|
|
1906
|
+
return null;
|
|
1706
1907
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1908
|
+
return validatedJob;
|
|
1909
|
+
},
|
|
1910
|
+
async saveJob(job) {
|
|
1911
|
+
const componentType = getJobComponentType(job.id);
|
|
1912
|
+
const existing = await runtime.getComponent(
|
|
1913
|
+
runtime.agentId,
|
|
1914
|
+
componentType
|
|
1915
|
+
);
|
|
1916
|
+
const component = {
|
|
1917
|
+
id: existing?.id || uuidv42(),
|
|
1918
|
+
entityId: runtime.agentId,
|
|
1919
|
+
agentId: runtime.agentId,
|
|
1920
|
+
roomId: runtime.agentId,
|
|
1921
|
+
worldId: existing?.worldId || uuidv42(),
|
|
1922
|
+
sourceEntityId: runtime.agentId,
|
|
1923
|
+
type: componentType,
|
|
1924
|
+
createdAt: existing?.createdAt || job.createdAtMs,
|
|
1925
|
+
data: job
|
|
1926
|
+
};
|
|
1927
|
+
if (existing) {
|
|
1928
|
+
await runtime.updateComponent(component);
|
|
1929
|
+
} else {
|
|
1930
|
+
await runtime.createComponent(component);
|
|
1931
|
+
await addToIndex(runtime, job.id);
|
|
1725
1932
|
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
name: "{{agentName}}",
|
|
1736
|
-
content: {
|
|
1737
|
-
text: 'Deleted cron job "daily-check" (abc-123).\nThe job has been permanently removed and will no longer run.'
|
|
1738
|
-
}
|
|
1933
|
+
},
|
|
1934
|
+
async deleteJob(jobId) {
|
|
1935
|
+
const componentType = getJobComponentType(jobId);
|
|
1936
|
+
const existing = await runtime.getComponent(
|
|
1937
|
+
runtime.agentId,
|
|
1938
|
+
componentType
|
|
1939
|
+
);
|
|
1940
|
+
if (!existing) {
|
|
1941
|
+
return false;
|
|
1739
1942
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1943
|
+
await runtime.deleteComponent(existing.id);
|
|
1944
|
+
await removeFromIndex(runtime, jobId);
|
|
1945
|
+
return true;
|
|
1946
|
+
},
|
|
1947
|
+
async listJobs(filter) {
|
|
1948
|
+
const index = await getJobIndex(runtime);
|
|
1949
|
+
const jobs = [];
|
|
1950
|
+
for (const jobId of index.jobIds) {
|
|
1951
|
+
const job = await this.getJob(jobId);
|
|
1952
|
+
if (job && matchesFilter(job, filter)) {
|
|
1953
|
+
jobs.push(job);
|
|
1750
1954
|
}
|
|
1751
1955
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
lines.push(
|
|
1769
|
-
`\u2022 ${job.name}${lastStatus}
|
|
1770
|
-
ID: ${job.id}
|
|
1771
|
-
Schedule: ${scheduleStr}
|
|
1772
|
-
Status: ${statusStr}
|
|
1773
|
-
Next run: ${nextRun}
|
|
1774
|
-
Runs: ${job.state.runCount} | Errors: ${job.state.errorCount}`
|
|
1775
|
-
);
|
|
1776
|
-
}
|
|
1777
|
-
return lines.join("\n");
|
|
1956
|
+
jobs.sort((a, b) => {
|
|
1957
|
+
const aNext = a.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
|
|
1958
|
+
const bNext = b.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
|
|
1959
|
+
return aNext - bNext;
|
|
1960
|
+
});
|
|
1961
|
+
return jobs;
|
|
1962
|
+
},
|
|
1963
|
+
async getJobCount() {
|
|
1964
|
+
const index = await getJobIndex(runtime);
|
|
1965
|
+
return index.jobIds.length;
|
|
1966
|
+
},
|
|
1967
|
+
async hasJob(jobId) {
|
|
1968
|
+
const index = await getJobIndex(runtime);
|
|
1969
|
+
return index.jobIds.includes(jobId);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1778
1972
|
}
|
|
1779
|
-
function formatJobDetails(job) {
|
|
1780
|
-
const scheduleStr = formatSchedule(job.schedule);
|
|
1781
|
-
const statusStr = job.enabled ? "enabled" : "disabled";
|
|
1782
|
-
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
|
|
1783
|
-
const lastRun = job.state.lastRunAtMs ? new Date(job.state.lastRunAtMs).toLocaleString() : "never";
|
|
1784
|
-
let details = `Cron Job: ${job.name}
|
|
1785
1973
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
`;
|
|
1798
|
-
if (job.deleteAfterRun) {
|
|
1799
|
-
details += `Type: one-shot (will be deleted after successful run)
|
|
1800
|
-
`;
|
|
1801
|
-
}
|
|
1802
|
-
details += `
|
|
1803
|
-
Execution Stats:
|
|
1804
|
-
`;
|
|
1805
|
-
details += ` Next run: ${nextRun}
|
|
1806
|
-
`;
|
|
1807
|
-
details += ` Last run: ${lastRun}
|
|
1808
|
-
`;
|
|
1809
|
-
details += ` Total runs: ${job.state.runCount}
|
|
1810
|
-
`;
|
|
1811
|
-
details += ` Total errors: ${job.state.errorCount}
|
|
1812
|
-
`;
|
|
1813
|
-
if (job.state.lastStatus) {
|
|
1814
|
-
details += ` Last status: ${job.state.lastStatus}
|
|
1815
|
-
`;
|
|
1816
|
-
}
|
|
1817
|
-
if (job.state.lastError) {
|
|
1818
|
-
details += ` Last error: ${job.state.lastError}
|
|
1819
|
-
`;
|
|
1820
|
-
}
|
|
1821
|
-
if (job.state.lastDurationMs !== void 0) {
|
|
1822
|
-
details += ` Last duration: ${job.state.lastDurationMs}ms
|
|
1823
|
-
`;
|
|
1824
|
-
}
|
|
1825
|
-
details += `
|
|
1826
|
-
Payload Type: ${job.payload.kind}
|
|
1827
|
-
`;
|
|
1828
|
-
if (job.payload.kind === "prompt") {
|
|
1829
|
-
details += `Prompt: ${job.payload.text.slice(0, 200)}${job.payload.text.length > 200 ? "..." : ""}
|
|
1830
|
-
`;
|
|
1831
|
-
} else if (job.payload.kind === "action") {
|
|
1832
|
-
details += `Action: ${job.payload.actionName}
|
|
1833
|
-
`;
|
|
1834
|
-
} else if (job.payload.kind === "event") {
|
|
1835
|
-
details += `Event: ${job.payload.eventName}
|
|
1836
|
-
`;
|
|
1974
|
+
// src/services/cron-service.ts
|
|
1975
|
+
var CronService = class _CronService extends Service {
|
|
1976
|
+
static serviceType = CRON_SERVICE_TYPE;
|
|
1977
|
+
capabilityDescription = "Schedules and executes recurring or one-time cron jobs";
|
|
1978
|
+
cronConfig;
|
|
1979
|
+
storage;
|
|
1980
|
+
timerManager;
|
|
1981
|
+
initialized = false;
|
|
1982
|
+
constructor(runtime, config) {
|
|
1983
|
+
super(runtime);
|
|
1984
|
+
this.cronConfig = { ...DEFAULT_CRON_CONFIG, ...config };
|
|
1837
1985
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* Starts the cron service
|
|
1988
|
+
*/
|
|
1989
|
+
static async start(runtime) {
|
|
1990
|
+
const service = new _CronService(runtime);
|
|
1991
|
+
await service.initialize();
|
|
1992
|
+
return service;
|
|
1842
1993
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
var listCronsAction = {
|
|
1851
|
-
name: CronActions.LIST_CRONS,
|
|
1852
|
-
similes: [
|
|
1853
|
-
"SHOW_CRONS",
|
|
1854
|
-
"GET_CRONS",
|
|
1855
|
-
"VIEW_CRONS",
|
|
1856
|
-
"LIST_SCHEDULED_JOBS",
|
|
1857
|
-
"SHOW_SCHEDULED_JOBS",
|
|
1858
|
-
"MY_CRONS",
|
|
1859
|
-
"CRON_STATUS"
|
|
1860
|
-
],
|
|
1861
|
-
description: "Lists all cron jobs. Can filter by enabled status or show details of a specific job.",
|
|
1862
|
-
validate: async (_runtime, message) => {
|
|
1863
|
-
const text = message.content?.text?.toLowerCase() ?? "";
|
|
1864
|
-
const hasListKeyword = text.includes("list") || text.includes("show") || text.includes("view") || text.includes("get") || text.includes("what");
|
|
1865
|
-
const hasCronKeyword = text.includes("cron") || text.includes("scheduled") || text.includes("job") || text.includes("schedule");
|
|
1866
|
-
return hasListKeyword && hasCronKeyword;
|
|
1867
|
-
},
|
|
1868
|
-
handler: async (runtime, message, _state, options, callback) => {
|
|
1869
|
-
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
1870
|
-
if (!cronService) {
|
|
1871
|
-
await callback?.({
|
|
1872
|
-
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
1873
|
-
});
|
|
1874
|
-
return { success: false, error: "Cron service not available" };
|
|
1994
|
+
/**
|
|
1995
|
+
* Initializes the service, loading existing jobs and starting timers
|
|
1996
|
+
*/
|
|
1997
|
+
async initialize() {
|
|
1998
|
+
if (this.initialized) {
|
|
1999
|
+
return;
|
|
1875
2000
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2001
|
+
this.storage = getCronStorage(this.runtime);
|
|
2002
|
+
this.timerManager = new TimerManager(this.cronConfig, async (jobId) => {
|
|
2003
|
+
await this.handleJobDue(jobId);
|
|
2004
|
+
});
|
|
2005
|
+
const jobs = await this.storage.listJobs({ includeDisabled: true });
|
|
2006
|
+
for (const job of jobs) {
|
|
2007
|
+
const nowMs = Date.now();
|
|
2008
|
+
const nextRunAtMs = computeNextRunAtMs(job.schedule, nowMs);
|
|
2009
|
+
if (job.state.nextRunAtMs !== nextRunAtMs) {
|
|
2010
|
+
job.state.nextRunAtMs = nextRunAtMs;
|
|
2011
|
+
await this.storage.saveJob(job);
|
|
1886
2012
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
});
|
|
1890
|
-
return { success: true, data: { job } };
|
|
1891
|
-
}
|
|
1892
|
-
const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
|
|
1893
|
-
if (nameMatch) {
|
|
1894
|
-
const jobName = nameMatch[1].toLowerCase();
|
|
1895
|
-
const jobs2 = await cronService.listJobs({ includeDisabled: true });
|
|
1896
|
-
const job = jobs2.find((j) => j.name.toLowerCase().includes(jobName));
|
|
1897
|
-
if (!job) {
|
|
1898
|
-
await callback?.({
|
|
1899
|
-
text: `No cron job found with name containing: ${jobName}`
|
|
1900
|
-
});
|
|
1901
|
-
return { success: false, error: "Job not found" };
|
|
2013
|
+
if (job.enabled) {
|
|
2014
|
+
this.timerManager.trackJob(job);
|
|
1902
2015
|
}
|
|
1903
|
-
await callback?.({
|
|
1904
|
-
text: formatJobDetails(job)
|
|
1905
|
-
});
|
|
1906
|
-
return { success: true, data: { job } };
|
|
1907
2016
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
filter.includeDisabled = true;
|
|
2017
|
+
if (this.cronConfig.catchUpMissedJobs) {
|
|
2018
|
+
await this.handleMissedJobs(jobs);
|
|
1911
2019
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2020
|
+
this.timerManager.start();
|
|
2021
|
+
await startHeartbeat(this.runtime);
|
|
2022
|
+
this.initialized = true;
|
|
2023
|
+
logger.info(
|
|
2024
|
+
`[CronService] Started for agent ${this.runtime.agentId} with ${jobs.length} jobs`
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Stops the cron service
|
|
2029
|
+
*/
|
|
2030
|
+
async stop() {
|
|
2031
|
+
if (this.timerManager) {
|
|
2032
|
+
this.timerManager.stop();
|
|
1915
2033
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2034
|
+
this.initialized = false;
|
|
2035
|
+
logger.info(`[CronService] Stopped for agent ${this.runtime.agentId}`);
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Gets the service configuration
|
|
2039
|
+
*/
|
|
2040
|
+
getConfig() {
|
|
2041
|
+
return { ...this.cronConfig };
|
|
2042
|
+
}
|
|
2043
|
+
// ============================================================================
|
|
2044
|
+
// CRUD OPERATIONS
|
|
2045
|
+
// ============================================================================
|
|
2046
|
+
/**
|
|
2047
|
+
* Creates a new cron job
|
|
2048
|
+
* @param input Job creation input
|
|
2049
|
+
* @returns The created job
|
|
2050
|
+
* @throws Error if validation fails or max jobs exceeded
|
|
2051
|
+
*/
|
|
2052
|
+
async createJob(input) {
|
|
2053
|
+
const scheduleError = validateSchedule(input.schedule, this.cronConfig);
|
|
2054
|
+
if (scheduleError) {
|
|
2055
|
+
throw new Error(`Invalid schedule: ${scheduleError}`);
|
|
1919
2056
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
2057
|
+
const currentCount = await this.storage.getJobCount();
|
|
2058
|
+
if (currentCount >= this.cronConfig.maxJobsPerAgent) {
|
|
2059
|
+
throw new Error(
|
|
2060
|
+
`Maximum jobs limit reached (${this.cronConfig.maxJobsPerAgent}). Delete some jobs before creating new ones.`
|
|
2061
|
+
);
|
|
1922
2062
|
}
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2063
|
+
const nowMs = Date.now();
|
|
2064
|
+
const job = {
|
|
2065
|
+
id: uuidv43(),
|
|
2066
|
+
name: input.name,
|
|
2067
|
+
description: input.description,
|
|
2068
|
+
// Explicitly default to true if not provided - jobs are enabled by default
|
|
2069
|
+
enabled: input.enabled ?? true,
|
|
2070
|
+
// Explicitly default to false - jobs persist after run by default
|
|
2071
|
+
deleteAfterRun: input.deleteAfterRun ?? false,
|
|
2072
|
+
createdAtMs: nowMs,
|
|
2073
|
+
updatedAtMs: nowMs,
|
|
2074
|
+
schedule: input.schedule,
|
|
2075
|
+
payload: input.payload,
|
|
2076
|
+
tags: input.tags,
|
|
2077
|
+
metadata: input.metadata,
|
|
2078
|
+
state: {
|
|
2079
|
+
nextRunAtMs: computeNextRunAtMs(input.schedule, nowMs),
|
|
2080
|
+
runCount: input.state?.runCount ?? 0,
|
|
2081
|
+
errorCount: input.state?.errorCount ?? 0,
|
|
2082
|
+
...input.state
|
|
1932
2083
|
}
|
|
1933
2084
|
};
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
],
|
|
1948
|
-
[
|
|
1949
|
-
{
|
|
1950
|
-
name: "{{user1}}",
|
|
1951
|
-
content: { text: "Show all crons including disabled" }
|
|
1952
|
-
},
|
|
1953
|
-
{
|
|
1954
|
-
name: "{{agentName}}",
|
|
1955
|
-
content: {
|
|
1956
|
-
text: "Found 3 cron jobs:\n\n\u2022 Daily news check\n ID: abc-123\n Schedule: cron: 0 9 * * *\n Status: enabled\n ...\n\n\u2022 Old backup job\n ID: xyz-789\n Schedule: every 1 day\n Status: disabled\n ..."
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
]
|
|
1960
|
-
]
|
|
1961
|
-
};
|
|
1962
|
-
|
|
1963
|
-
// src/actions/run-cron.ts
|
|
1964
|
-
function extractJobIdentifier3(text) {
|
|
1965
|
-
const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(text);
|
|
1966
|
-
if (idMatch) {
|
|
1967
|
-
return { id: idMatch[1] };
|
|
1968
|
-
}
|
|
1969
|
-
const quotedMatch = /["']([^"']+)["']/i.exec(text);
|
|
1970
|
-
if (quotedMatch) {
|
|
1971
|
-
return { name: quotedMatch[1] };
|
|
1972
|
-
}
|
|
1973
|
-
const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
|
|
1974
|
-
if (namedMatch) {
|
|
1975
|
-
return { name: namedMatch[1] };
|
|
2085
|
+
const execError = validateJobExecutability(this.runtime, job);
|
|
2086
|
+
if (execError) {
|
|
2087
|
+
throw new Error(`Job cannot be executed: ${execError}`);
|
|
2088
|
+
}
|
|
2089
|
+
await this.storage.saveJob(job);
|
|
2090
|
+
if (job.enabled) {
|
|
2091
|
+
this.timerManager.trackJob(job);
|
|
2092
|
+
}
|
|
2093
|
+
await this.emitCronEvent(CronEvents.CRON_CREATED, job);
|
|
2094
|
+
logger.info(
|
|
2095
|
+
`[CronService] Created job "${job.name}" (${job.id}) - ${formatSchedule(job.schedule)}`
|
|
2096
|
+
);
|
|
2097
|
+
return job;
|
|
1976
2098
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
],
|
|
1989
|
-
description: "Manually runs a cron job immediately, regardless of its schedule. Useful for testing or one-off execution.",
|
|
1990
|
-
validate: async (_runtime, message) => {
|
|
1991
|
-
const text = message.content?.text?.toLowerCase() ?? "";
|
|
1992
|
-
const hasRunKeyword = text.includes("run") || text.includes("execute") || text.includes("trigger") || text.includes("fire");
|
|
1993
|
-
const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
|
|
1994
|
-
const isCreateIntent = text.includes("run every") || text.includes("runs every");
|
|
1995
|
-
return hasRunKeyword && hasCronKeyword && !isCreateIntent;
|
|
1996
|
-
},
|
|
1997
|
-
handler: async (runtime, message, _state, options, callback) => {
|
|
1998
|
-
const cronService = runtime.getService(CRON_SERVICE_TYPE);
|
|
1999
|
-
if (!cronService) {
|
|
2000
|
-
await callback?.({
|
|
2001
|
-
text: "Cron service is not available. Please ensure the plugin is loaded."
|
|
2002
|
-
});
|
|
2003
|
-
return { success: false, error: "Cron service not available" };
|
|
2099
|
+
/**
|
|
2100
|
+
* Updates an existing cron job
|
|
2101
|
+
* @param jobId The job ID to update
|
|
2102
|
+
* @param patch The fields to update
|
|
2103
|
+
* @returns The updated job
|
|
2104
|
+
* @throws Error if job not found or validation fails
|
|
2105
|
+
*/
|
|
2106
|
+
async updateJob(jobId, patch) {
|
|
2107
|
+
const existing = await this.storage.getJob(jobId);
|
|
2108
|
+
if (!existing) {
|
|
2109
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
2004
2110
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
const identifier = extractJobIdentifier3(text);
|
|
2010
|
-
if (identifier.id) {
|
|
2011
|
-
jobId = identifier.id;
|
|
2012
|
-
} else if (identifier.name) {
|
|
2013
|
-
const jobs = await cronService.listJobs({ includeDisabled: true });
|
|
2014
|
-
const job = jobs.find(
|
|
2015
|
-
(j) => j.name.toLowerCase() === identifier.name.toLowerCase()
|
|
2016
|
-
);
|
|
2017
|
-
if (!job) {
|
|
2018
|
-
await callback?.({
|
|
2019
|
-
text: `No cron job found with name: ${identifier.name}`
|
|
2020
|
-
});
|
|
2021
|
-
return { success: false, error: "Job not found" };
|
|
2022
|
-
}
|
|
2023
|
-
jobId = job.id;
|
|
2024
|
-
jobName = job.name;
|
|
2111
|
+
if (patch.schedule) {
|
|
2112
|
+
const scheduleError = validateSchedule(patch.schedule, this.cronConfig);
|
|
2113
|
+
if (scheduleError) {
|
|
2114
|
+
throw new Error(`Invalid schedule: ${scheduleError}`);
|
|
2025
2115
|
}
|
|
2026
2116
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2117
|
+
const nowMs = Date.now();
|
|
2118
|
+
const updated = {
|
|
2119
|
+
...existing,
|
|
2120
|
+
...patch,
|
|
2121
|
+
id: existing.id,
|
|
2122
|
+
// Ensure ID is immutable
|
|
2123
|
+
createdAtMs: existing.createdAtMs,
|
|
2124
|
+
// Ensure createdAt is immutable
|
|
2125
|
+
updatedAtMs: nowMs,
|
|
2126
|
+
state: {
|
|
2127
|
+
...existing.state,
|
|
2128
|
+
...patch.state
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
if (patch.schedule || patch.enabled === true && !existing.enabled) {
|
|
2132
|
+
updated.state.nextRunAtMs = computeNextRunAtMs(updated.schedule, nowMs);
|
|
2032
2133
|
}
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2134
|
+
if (patch.enabled === false) {
|
|
2135
|
+
updated.state.nextRunAtMs = void 0;
|
|
2136
|
+
updated.state.runningAtMs = void 0;
|
|
2137
|
+
}
|
|
2138
|
+
if (patch.payload) {
|
|
2139
|
+
const execError = validateJobExecutability(this.runtime, updated);
|
|
2140
|
+
if (execError) {
|
|
2141
|
+
throw new Error(`Job cannot be executed: ${execError}`);
|
|
2037
2142
|
}
|
|
2038
2143
|
}
|
|
2039
|
-
|
|
2040
|
-
if (
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
return { success: false, error: result.error };
|
|
2144
|
+
await this.storage.saveJob(updated);
|
|
2145
|
+
if (updated.enabled) {
|
|
2146
|
+
this.timerManager.trackJob(updated);
|
|
2147
|
+
} else {
|
|
2148
|
+
this.timerManager.untrackJob(jobId);
|
|
2045
2149
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2150
|
+
await this.emitCronEvent(CronEvents.CRON_UPDATED, updated);
|
|
2151
|
+
logger.info(`[CronService] Updated job "${updated.name}" (${updated.id})`);
|
|
2152
|
+
return updated;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Deletes a cron job
|
|
2156
|
+
* @param jobId The job ID to delete
|
|
2157
|
+
* @returns true if deleted, false if not found
|
|
2158
|
+
*/
|
|
2159
|
+
async deleteJob(jobId) {
|
|
2160
|
+
const existing = await this.storage.getJob(jobId);
|
|
2161
|
+
if (!existing) {
|
|
2162
|
+
return false;
|
|
2057
2163
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2164
|
+
this.timerManager.untrackJob(jobId);
|
|
2165
|
+
const deleted = await this.storage.deleteJob(jobId);
|
|
2166
|
+
if (deleted) {
|
|
2167
|
+
await this.emitCronEvent(CronEvents.CRON_DELETED, existing);
|
|
2168
|
+
logger.info(
|
|
2169
|
+
`[CronService] Deleted job "${existing.name}" (${existing.id})`
|
|
2170
|
+
);
|
|
2061
2171
|
}
|
|
2062
|
-
|
|
2063
|
-
text: responseText
|
|
2064
|
-
});
|
|
2065
|
-
return {
|
|
2066
|
-
success: result.status === "ok",
|
|
2067
|
-
data: {
|
|
2068
|
-
jobId,
|
|
2069
|
-
jobName,
|
|
2070
|
-
result
|
|
2071
|
-
}
|
|
2072
|
-
};
|
|
2073
|
-
},
|
|
2074
|
-
examples: [
|
|
2075
|
-
[
|
|
2076
|
-
{
|
|
2077
|
-
name: "{{user1}}",
|
|
2078
|
-
content: { text: "Run the cron job called daily-check now" }
|
|
2079
|
-
},
|
|
2080
|
-
{
|
|
2081
|
-
name: "{{agentName}}",
|
|
2082
|
-
content: {
|
|
2083
|
-
text: 'Ran cron job "daily-check" (abc-123)\nStatus: ok\nDuration: 1250ms\n\nOutput:\nDaily check completed successfully. All systems operational.'
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
],
|
|
2087
|
-
[
|
|
2088
|
-
{
|
|
2089
|
-
name: "{{user1}}",
|
|
2090
|
-
content: { text: "Execute cron abc-123-def-456" }
|
|
2091
|
-
},
|
|
2092
|
-
{
|
|
2093
|
-
name: "{{agentName}}",
|
|
2094
|
-
content: {
|
|
2095
|
-
text: 'Ran cron job "status-checker" (abc-123-def-456)\nStatus: ok\nDuration: 850ms'
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
]
|
|
2099
|
-
]
|
|
2100
|
-
};
|
|
2101
|
-
|
|
2102
|
-
// src/providers/cron-context.ts
|
|
2103
|
-
function formatJobForContext(job) {
|
|
2104
|
-
const scheduleStr = formatSchedule(job.schedule);
|
|
2105
|
-
const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "not scheduled";
|
|
2106
|
-
let line = `- ${job.name} (${scheduleStr})`;
|
|
2107
|
-
if (!job.enabled) {
|
|
2108
|
-
line += " [disabled]";
|
|
2109
|
-
} else {
|
|
2110
|
-
line += ` - next: ${nextRun}`;
|
|
2172
|
+
return deleted;
|
|
2111
2173
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2174
|
+
/**
|
|
2175
|
+
* Gets a job by ID
|
|
2176
|
+
* @param jobId The job ID
|
|
2177
|
+
* @returns The job or null if not found
|
|
2178
|
+
*/
|
|
2179
|
+
async getJob(jobId) {
|
|
2180
|
+
return this.storage.getJob(jobId);
|
|
2114
2181
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Lists all jobs, optionally filtered
|
|
2184
|
+
* @param filter Optional filter criteria
|
|
2185
|
+
* @returns Array of matching jobs
|
|
2186
|
+
*/
|
|
2187
|
+
async listJobs(filter) {
|
|
2188
|
+
return this.storage.listJobs(filter);
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Gets the count of jobs
|
|
2192
|
+
*/
|
|
2193
|
+
async getJobCount() {
|
|
2194
|
+
return this.storage.getJobCount();
|
|
2195
|
+
}
|
|
2196
|
+
// ============================================================================
|
|
2197
|
+
// EXECUTION
|
|
2198
|
+
// ============================================================================
|
|
2199
|
+
/**
|
|
2200
|
+
* Manually runs a job immediately
|
|
2201
|
+
* @param jobId The job ID to run
|
|
2202
|
+
* @param mode 'force' to run even if disabled, 'due' to only run if due
|
|
2203
|
+
* @returns Execution result
|
|
2204
|
+
*/
|
|
2205
|
+
async runJob(jobId, mode = "force") {
|
|
2206
|
+
const job = await this.storage.getJob(jobId);
|
|
2207
|
+
if (!job) {
|
|
2208
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
2136
2209
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
);
|
|
2148
|
-
const failedJobs = jobs.filter((j) => j.state.lastStatus === "error");
|
|
2149
|
-
const lines = [];
|
|
2150
|
-
if (jobs.length === 0) {
|
|
2151
|
-
lines.push("No cron jobs are scheduled.");
|
|
2152
|
-
} else {
|
|
2153
|
-
lines.push(`Scheduled Jobs (${enabledJobs.length} active, ${disabledJobs.length} disabled):`);
|
|
2154
|
-
if (upcomingJobs.length > 0) {
|
|
2155
|
-
lines.push("\nUpcoming (next hour):");
|
|
2156
|
-
for (const job of upcomingJobs.slice(0, 5)) {
|
|
2157
|
-
lines.push(formatJobForContext(job));
|
|
2158
|
-
}
|
|
2159
|
-
if (upcomingJobs.length > 5) {
|
|
2160
|
-
lines.push(` ... and ${upcomingJobs.length - 5} more`);
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
if (failedJobs.length > 0) {
|
|
2164
|
-
lines.push("\nRecently failed:");
|
|
2165
|
-
for (const job of failedJobs.slice(0, 3)) {
|
|
2166
|
-
lines.push(`- ${job.name}: ${job.state.lastError || "unknown error"}`);
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
if (enabledJobs.length <= 10 && enabledJobs.length > 0) {
|
|
2170
|
-
lines.push("\nAll active jobs:");
|
|
2171
|
-
for (const job of enabledJobs) {
|
|
2172
|
-
lines.push(formatJobForContext(job));
|
|
2173
|
-
}
|
|
2174
|
-
} else if (enabledJobs.length > 10) {
|
|
2175
|
-
lines.push(`
|
|
2176
|
-
${enabledJobs.length} active jobs total. Use "list crons" to see all.`);
|
|
2210
|
+
if (mode === "due") {
|
|
2211
|
+
const nowMs = Date.now();
|
|
2212
|
+
const nextRunAtMs = job.state.nextRunAtMs;
|
|
2213
|
+
if (!nextRunAtMs || nowMs < nextRunAtMs) {
|
|
2214
|
+
return {
|
|
2215
|
+
ran: false,
|
|
2216
|
+
status: "skipped",
|
|
2217
|
+
durationMs: 0,
|
|
2218
|
+
error: "Job is not due yet"
|
|
2219
|
+
};
|
|
2177
2220
|
}
|
|
2178
2221
|
}
|
|
2222
|
+
if (!job.enabled && mode !== "force") {
|
|
2223
|
+
return {
|
|
2224
|
+
ran: false,
|
|
2225
|
+
status: "skipped",
|
|
2226
|
+
durationMs: 0,
|
|
2227
|
+
error: "Job is disabled"
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
const result = await this.executeJobInternal(job);
|
|
2179
2231
|
return {
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
hasCronService: true,
|
|
2183
|
-
cronJobCount: jobs.length,
|
|
2184
|
-
enabledJobCount: enabledJobs.length,
|
|
2185
|
-
disabledJobCount: disabledJobs.length,
|
|
2186
|
-
upcomingJobCount: upcomingJobs.length,
|
|
2187
|
-
failedJobCount: failedJobs.length
|
|
2188
|
-
},
|
|
2189
|
-
data: {
|
|
2190
|
-
available: true,
|
|
2191
|
-
jobs: jobs.map((j) => ({
|
|
2192
|
-
id: j.id,
|
|
2193
|
-
name: j.name,
|
|
2194
|
-
enabled: j.enabled,
|
|
2195
|
-
schedule: j.schedule,
|
|
2196
|
-
nextRunAtMs: j.state.nextRunAtMs,
|
|
2197
|
-
lastStatus: j.state.lastStatus
|
|
2198
|
-
})),
|
|
2199
|
-
upcoming: upcomingJobs.map((j) => j.id),
|
|
2200
|
-
failed: failedJobs.map((j) => j.id),
|
|
2201
|
-
recent: recentJobs.map((j) => j.id)
|
|
2202
|
-
}
|
|
2232
|
+
ran: true,
|
|
2233
|
+
...result
|
|
2203
2234
|
};
|
|
2204
2235
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
//
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
config: status.config
|
|
2223
|
-
});
|
|
2224
|
-
}
|
|
2225
|
-
async function handleCronList(req, res, runtime) {
|
|
2226
|
-
const includeDisabled = req.body?.includeDisabled === true;
|
|
2227
|
-
const svc = getCronService(runtime);
|
|
2228
|
-
const jobs = await svc.listJobs({ includeDisabled });
|
|
2229
|
-
res.json({ jobs });
|
|
2230
|
-
}
|
|
2231
|
-
async function handleCronAdd(req, res, runtime) {
|
|
2232
|
-
const body = req.body ?? {};
|
|
2233
|
-
const normalized = normalizeCronJobCreate(body);
|
|
2234
|
-
if (!normalized) {
|
|
2235
|
-
res.status(400).json({ error: "Invalid job input" });
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
const svc = getCronService(runtime);
|
|
2239
|
-
const job = await svc.createJob(normalized);
|
|
2240
|
-
res.json({ job });
|
|
2241
|
-
}
|
|
2242
|
-
async function handleCronUpdate(req, res, runtime) {
|
|
2243
|
-
const body = req.body ?? {};
|
|
2244
|
-
const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
|
|
2245
|
-
if (!jobId) {
|
|
2246
|
-
res.status(400).json({ error: "Missing jobId" });
|
|
2247
|
-
return;
|
|
2236
|
+
// ============================================================================
|
|
2237
|
+
// INTERNAL METHODS
|
|
2238
|
+
// ============================================================================
|
|
2239
|
+
/**
|
|
2240
|
+
* Handles a job becoming due (called by timer manager)
|
|
2241
|
+
*/
|
|
2242
|
+
async handleJobDue(jobId) {
|
|
2243
|
+
const job = await this.storage.getJob(jobId);
|
|
2244
|
+
if (!job) {
|
|
2245
|
+
this.timerManager.untrackJob(jobId);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
if (!job.enabled) {
|
|
2249
|
+
this.timerManager.untrackJob(jobId);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
await this.executeJobInternal(job);
|
|
2248
2253
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2254
|
+
/**
|
|
2255
|
+
* Internal job execution with state management
|
|
2256
|
+
*/
|
|
2257
|
+
async executeJobInternal(job) {
|
|
2258
|
+
const nowMs = Date.now();
|
|
2259
|
+
job.state.runningAtMs = nowMs;
|
|
2260
|
+
await this.storage.saveJob(job);
|
|
2261
|
+
logger.debug(`[CronService] Executing job "${job.name}" (${job.id})`);
|
|
2262
|
+
const result = await executeJob(this.runtime, job, this.cronConfig);
|
|
2263
|
+
job.state.runningAtMs = void 0;
|
|
2264
|
+
job.state.lastRunAtMs = nowMs;
|
|
2265
|
+
job.state.lastStatus = result.status;
|
|
2266
|
+
job.state.lastDurationMs = result.durationMs;
|
|
2267
|
+
if (result.status === "ok") {
|
|
2268
|
+
job.state.runCount += 1;
|
|
2269
|
+
job.state.lastError = void 0;
|
|
2270
|
+
} else {
|
|
2271
|
+
job.state.errorCount += 1;
|
|
2272
|
+
job.state.lastError = result.error;
|
|
2273
|
+
}
|
|
2274
|
+
const nextNowMs = Date.now();
|
|
2275
|
+
job.state.nextRunAtMs = computeNextRunAtMs(job.schedule, nextNowMs);
|
|
2276
|
+
job.updatedAtMs = nextNowMs;
|
|
2277
|
+
if (job.deleteAfterRun && result.status === "ok") {
|
|
2278
|
+
await this.storage.deleteJob(job.id);
|
|
2279
|
+
this.timerManager.untrackJob(job.id);
|
|
2280
|
+
logger.info(
|
|
2281
|
+
`[CronService] Deleted one-shot job "${job.name}" (${job.id}) after successful execution`
|
|
2282
|
+
);
|
|
2283
|
+
} else {
|
|
2284
|
+
await this.storage.saveJob(job);
|
|
2285
|
+
this.timerManager.markFinished(job.id, job);
|
|
2286
|
+
}
|
|
2287
|
+
const eventName = result.status === "ok" ? CronEvents.CRON_FIRED : CronEvents.CRON_FAILED;
|
|
2288
|
+
await this.emitCronEvent(eventName, job, result);
|
|
2289
|
+
logger.info(
|
|
2290
|
+
`[CronService] Job "${job.name}" (${job.id}) completed with status: ${result.status}` + (result.error ? ` - ${result.error}` : "")
|
|
2291
|
+
);
|
|
2292
|
+
return result;
|
|
2253
2293
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2294
|
+
/**
|
|
2295
|
+
* Handles catch-up for jobs that may have been missed while the service was stopped
|
|
2296
|
+
*/
|
|
2297
|
+
async handleMissedJobs(jobs) {
|
|
2298
|
+
const nowMs = Date.now();
|
|
2299
|
+
const windowStart = nowMs - this.cronConfig.catchUpWindowMs;
|
|
2300
|
+
for (const job of jobs) {
|
|
2301
|
+
if (!job.enabled) {
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const lastRunAtMs = job.state.lastRunAtMs ?? 0;
|
|
2305
|
+
const nextRunAtMs = job.state.nextRunAtMs;
|
|
2306
|
+
if (nextRunAtMs && nextRunAtMs >= windowStart && nextRunAtMs < nowMs && lastRunAtMs < nextRunAtMs) {
|
|
2307
|
+
logger.info(
|
|
2308
|
+
`[CronService] Catching up missed job "${job.name}" (${job.id}) that was due at ${new Date(nextRunAtMs).toISOString()}`
|
|
2309
|
+
);
|
|
2310
|
+
await this.executeJobInternal(job);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2264
2313
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2314
|
+
/**
|
|
2315
|
+
* Emits a cron event
|
|
2316
|
+
*/
|
|
2317
|
+
async emitCronEvent(eventName, job, result) {
|
|
2318
|
+
const eventData = {
|
|
2319
|
+
jobId: job.id,
|
|
2320
|
+
jobName: job.name,
|
|
2321
|
+
schedule: job.schedule
|
|
2322
|
+
};
|
|
2323
|
+
if (result) {
|
|
2324
|
+
eventData.result = {
|
|
2325
|
+
status: result.status,
|
|
2326
|
+
durationMs: result.durationMs,
|
|
2327
|
+
output: result.output,
|
|
2328
|
+
error: result.error
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
await this.runtime.emitEvent(eventName, {
|
|
2332
|
+
runtime: this.runtime,
|
|
2333
|
+
source: `cron:${job.id}`,
|
|
2334
|
+
...eventData
|
|
2335
|
+
});
|
|
2275
2336
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2337
|
+
// ============================================================================
|
|
2338
|
+
// STATUS AND DIAGNOSTICS
|
|
2339
|
+
// ============================================================================
|
|
2340
|
+
/**
|
|
2341
|
+
* Gets the service status
|
|
2342
|
+
*/
|
|
2343
|
+
async getStatus() {
|
|
2344
|
+
return {
|
|
2345
|
+
initialized: this.initialized,
|
|
2346
|
+
jobCount: await this.storage.getJobCount(),
|
|
2347
|
+
trackedJobCount: this.timerManager?.getTrackedJobCount() ?? 0,
|
|
2348
|
+
config: this.cronConfig
|
|
2349
|
+
};
|
|
2288
2350
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2351
|
+
/**
|
|
2352
|
+
* Performs a health check
|
|
2353
|
+
*/
|
|
2354
|
+
async healthCheck() {
|
|
2355
|
+
const issues = [];
|
|
2356
|
+
if (!this.initialized) {
|
|
2357
|
+
issues.push("Service not initialized");
|
|
2358
|
+
}
|
|
2359
|
+
if (!this.storage) {
|
|
2360
|
+
issues.push("Storage not available");
|
|
2361
|
+
}
|
|
2362
|
+
if (!this.timerManager) {
|
|
2363
|
+
issues.push("Timer manager not available");
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
healthy: issues.length === 0,
|
|
2367
|
+
issues
|
|
2368
|
+
};
|
|
2293
2369
|
}
|
|
2294
|
-
|
|
2295
|
-
const entries = await readCronRunLogEntries(logPath, { limit, jobId });
|
|
2296
|
-
res.json({ entries });
|
|
2297
|
-
}
|
|
2298
|
-
var cronRoutes = [
|
|
2299
|
-
{ type: "POST", path: "/api/cron/status", handler: handleCronStatus },
|
|
2300
|
-
{ type: "POST", path: "/api/cron/list", handler: handleCronList },
|
|
2301
|
-
{ type: "POST", path: "/api/cron/add", handler: handleCronAdd },
|
|
2302
|
-
{ type: "POST", path: "/api/cron/update", handler: handleCronUpdate },
|
|
2303
|
-
{ type: "POST", path: "/api/cron/remove", handler: handleCronRemove },
|
|
2304
|
-
{ type: "POST", path: "/api/cron/run", handler: handleCronRun },
|
|
2305
|
-
{ type: "POST", path: "/api/cron/runs", handler: handleCronRuns }
|
|
2306
|
-
];
|
|
2370
|
+
};
|
|
2307
2371
|
|
|
2308
2372
|
// src/cli/index.ts
|
|
2309
|
-
import {
|
|
2373
|
+
import { defineCliCommand, registerCliCommand } from "@elizaos/plugin-cli";
|
|
2310
2374
|
|
|
2311
2375
|
// src/cli/register.ts
|
|
2312
2376
|
var defaultLogger = {
|
|
@@ -2378,13 +2442,26 @@ function registerCronCli(ctx) {
|
|
|
2378
2442
|
process.exitCode = 1;
|
|
2379
2443
|
}
|
|
2380
2444
|
});
|
|
2381
|
-
cron.command("add").alias("create").description("Add a cron job").requiredOption("--name <name>", "Job name").option("--description <text>", "Optional description").option("--disabled", "Create job disabled", false).option(
|
|
2445
|
+
cron.command("add").alias("create").description("Add a cron job").requiredOption("--name <name>", "Job name").option("--description <text>", "Optional description").option("--disabled", "Create job disabled", false).option(
|
|
2446
|
+
"--delete-after-run",
|
|
2447
|
+
"Delete one-shot job after it succeeds",
|
|
2448
|
+
false
|
|
2449
|
+
).option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)").option("--every <duration>", "Run every duration (e.g. 10m, 1h)").option("--cron <expr>", "Cron expression (5-field)").option("--tz <iana>", "Timezone for cron expressions (IANA)").option("--prompt <text>", "Prompt to execute").option("--action <name>", "Action to execute").option("--event <name>", "Event to emit").option("--session <target>", "Session target: main or isolated").option("--system-event <text>", "System event text (main session)").option("--message <text>", "Agent message (isolated session)").option(
|
|
2450
|
+
"--wake <mode>",
|
|
2451
|
+
"Wake mode: now or next-heartbeat",
|
|
2452
|
+
"next-heartbeat"
|
|
2453
|
+
).option("--announce", "Enable announce delivery for isolated jobs", false).option(
|
|
2454
|
+
"--channel <name>",
|
|
2455
|
+
"Delivery channel (e.g. whatsapp, telegram, discord, last)"
|
|
2456
|
+
).option("--to <target>", "Delivery target (e.g. phone number, channel ID)").option("--agent <id>", "Agent ID to bind this job to").option("--json", "Output JSON", false).action(async (opts) => {
|
|
2382
2457
|
const service = getCronService2(ctx);
|
|
2383
2458
|
if (!service) {
|
|
2384
2459
|
process.exitCode = 1;
|
|
2385
2460
|
return;
|
|
2386
2461
|
}
|
|
2387
|
-
const isOttoStyle = Boolean(
|
|
2462
|
+
const isOttoStyle = Boolean(
|
|
2463
|
+
opts.session || opts.systemEvent || opts.message
|
|
2464
|
+
);
|
|
2388
2465
|
const schedule = parseScheduleOpts(opts);
|
|
2389
2466
|
if (!schedule) {
|
|
2390
2467
|
logger2.error("Choose exactly one schedule: --at, --every, or --cron");
|
|
@@ -2403,14 +2480,22 @@ function registerCronCli(ctx) {
|
|
|
2403
2480
|
} else {
|
|
2404
2481
|
logger2.info(`Created job: ${job.id}`);
|
|
2405
2482
|
logger2.info(` Name: ${ottoInput.name}`);
|
|
2406
|
-
logger2.info(
|
|
2407
|
-
|
|
2408
|
-
|
|
2483
|
+
logger2.info(
|
|
2484
|
+
` Session: ${ottoInput.sessionTarget ?? "n/a"}`
|
|
2485
|
+
);
|
|
2486
|
+
logger2.info(
|
|
2487
|
+
` Wake: ${ottoInput.wakeMode ?? "next-heartbeat"}`
|
|
2488
|
+
);
|
|
2489
|
+
logger2.info(
|
|
2490
|
+
` Next run: ${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "N/A"}`
|
|
2491
|
+
);
|
|
2409
2492
|
}
|
|
2410
2493
|
} else {
|
|
2411
2494
|
const payload = parsePayloadOpts(opts);
|
|
2412
2495
|
if (!payload) {
|
|
2413
|
-
logger2.error(
|
|
2496
|
+
logger2.error(
|
|
2497
|
+
"Choose exactly one payload: --prompt, --action, --event, --system-event, or --message"
|
|
2498
|
+
);
|
|
2414
2499
|
process.exitCode = 1;
|
|
2415
2500
|
return;
|
|
2416
2501
|
}
|
|
@@ -2429,7 +2514,9 @@ function registerCronCli(ctx) {
|
|
|
2429
2514
|
logger2.info(`Created job: ${job.id}`);
|
|
2430
2515
|
logger2.info(` Name: ${job.name}`);
|
|
2431
2516
|
logger2.info(` Enabled: ${job.enabled}`);
|
|
2432
|
-
logger2.info(
|
|
2517
|
+
logger2.info(
|
|
2518
|
+
` Next run: ${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "N/A"}`
|
|
2519
|
+
);
|
|
2433
2520
|
}
|
|
2434
2521
|
}
|
|
2435
2522
|
});
|
|
@@ -2525,7 +2612,9 @@ function parseScheduleOpts(opts) {
|
|
|
2525
2612
|
const at = typeof opts.at === "string" ? opts.at : "";
|
|
2526
2613
|
const every = typeof opts.every === "string" ? opts.every : "";
|
|
2527
2614
|
const cronExpr = typeof opts.cron === "string" ? opts.cron : "";
|
|
2528
|
-
const chosen = [Boolean(at), Boolean(every), Boolean(cronExpr)].filter(
|
|
2615
|
+
const chosen = [Boolean(at), Boolean(every), Boolean(cronExpr)].filter(
|
|
2616
|
+
Boolean
|
|
2617
|
+
).length;
|
|
2529
2618
|
if (chosen === 0) return null;
|
|
2530
2619
|
if (chosen > 1) return null;
|
|
2531
2620
|
if (at) {
|
|
@@ -2548,7 +2637,9 @@ function parsePayloadOpts(opts) {
|
|
|
2548
2637
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
2549
2638
|
const action = typeof opts.action === "string" ? opts.action : "";
|
|
2550
2639
|
const event = typeof opts.event === "string" ? opts.event : "";
|
|
2551
|
-
const chosen = [Boolean(prompt), Boolean(action), Boolean(event)].filter(
|
|
2640
|
+
const chosen = [Boolean(prompt), Boolean(action), Boolean(event)].filter(
|
|
2641
|
+
Boolean
|
|
2642
|
+
).length;
|
|
2552
2643
|
if (chosen === 0) return null;
|
|
2553
2644
|
if (chosen > 1) return null;
|
|
2554
2645
|
if (prompt) {
|
|
@@ -2655,8 +2746,12 @@ function printCronList(jobs, log) {
|
|
|
2655
2746
|
log("No cron jobs.");
|
|
2656
2747
|
return;
|
|
2657
2748
|
}
|
|
2658
|
-
log(
|
|
2659
|
-
|
|
2749
|
+
log(
|
|
2750
|
+
"ID | Name | Status | Schedule | Next Run"
|
|
2751
|
+
);
|
|
2752
|
+
log(
|
|
2753
|
+
"-------------------------------------|--------------------------|----------|----------------------------------|--------------------"
|
|
2754
|
+
);
|
|
2660
2755
|
for (const job of jobs) {
|
|
2661
2756
|
const id = job.id.padEnd(36);
|
|
2662
2757
|
const name = (job.name || "(unnamed)").slice(0, 24).padEnd(24);
|