@elizaos/plugin-cron 2.0.0-alpha.3

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.
@@ -0,0 +1,1311 @@
1
+ import {
2
+ __export
3
+ } from "./chunk-MLKGABMK.js";
4
+
5
+ // src/otto/index.ts
6
+ var otto_exports = {};
7
+ __export(otto_exports, {
8
+ appendCronRunLog: () => appendCronRunLog,
9
+ applyJobPatch: () => applyJobPatch,
10
+ assertDeliverySupport: () => assertDeliverySupport,
11
+ assertSupportedJobSpec: () => assertSupportedJobSpec,
12
+ executeOttoJob: () => executeOttoJob,
13
+ isOttoPayload: () => isOttoPayload,
14
+ loadCronStore: () => loadCronStore,
15
+ migrateLegacyCronPayload: () => migrateLegacyCronPayload,
16
+ normalizeCronJobCreate: () => normalizeCronJobCreate,
17
+ normalizeCronJobInput: () => normalizeCronJobInput,
18
+ normalizeCronJobPatch: () => normalizeCronJobPatch,
19
+ normalizeOptionalAgentId: () => normalizeOptionalAgentId,
20
+ normalizeOptionalText: () => normalizeOptionalText,
21
+ normalizeRequiredName: () => normalizeRequiredName,
22
+ parseAbsoluteTimeMs: () => parseAbsoluteTimeMs,
23
+ readCronRunLogEntries: () => readCronRunLogEntries,
24
+ resolveCronDeliveryPlan: () => resolveCronDeliveryPlan,
25
+ resolveCronRunLogPath: () => resolveCronRunLogPath,
26
+ resolveCronStorePath: () => resolveCronStorePath,
27
+ saveCronStore: () => saveCronStore,
28
+ validateScheduleTimestamp: () => validateScheduleTimestamp
29
+ });
30
+
31
+ // src/otto/delivery.ts
32
+ function normalizeChannel(value) {
33
+ if (typeof value !== "string") {
34
+ return void 0;
35
+ }
36
+ const trimmed = value.trim().toLowerCase();
37
+ if (!trimmed) {
38
+ return void 0;
39
+ }
40
+ return trimmed;
41
+ }
42
+ function normalizeTo(value) {
43
+ if (typeof value !== "string") {
44
+ return void 0;
45
+ }
46
+ const trimmed = value.trim();
47
+ return trimmed ? trimmed : void 0;
48
+ }
49
+ function resolveCronDeliveryPlan(job) {
50
+ const payload = job.payload.kind === "agentTurn" ? job.payload : null;
51
+ const delivery = job.delivery;
52
+ const hasDelivery = delivery && typeof delivery === "object";
53
+ const rawMode = hasDelivery ? delivery.mode : void 0;
54
+ const mode = rawMode === "announce" ? "announce" : rawMode === "none" ? "none" : rawMode === "deliver" ? "announce" : void 0;
55
+ const payloadChannel = normalizeChannel(payload?.channel);
56
+ const payloadTo = normalizeTo(payload?.to);
57
+ const deliveryChannel = normalizeChannel(
58
+ delivery?.channel
59
+ );
60
+ const deliveryTo = normalizeTo(delivery?.to);
61
+ const channel = deliveryChannel ?? payloadChannel ?? "last";
62
+ const to = deliveryTo ?? payloadTo;
63
+ if (hasDelivery) {
64
+ const resolvedMode = mode ?? "none";
65
+ return {
66
+ mode: resolvedMode,
67
+ channel,
68
+ to,
69
+ source: "delivery",
70
+ requested: resolvedMode === "announce"
71
+ };
72
+ }
73
+ const legacyMode = payload?.deliver === true ? "explicit" : payload?.deliver === false ? "off" : "auto";
74
+ const hasExplicitTarget = Boolean(to);
75
+ const requested = legacyMode === "explicit" || legacyMode === "auto" && hasExplicitTarget;
76
+ return {
77
+ mode: requested ? "announce" : "none",
78
+ channel,
79
+ to,
80
+ source: "payload",
81
+ requested
82
+ };
83
+ }
84
+
85
+ // src/otto/detect.ts
86
+ function isOttoPayload(payload) {
87
+ const kind = payload.kind;
88
+ return kind === "systemEvent" || kind === "agentTurn";
89
+ }
90
+
91
+ // src/otto/executor.ts
92
+ import {
93
+ ChannelType as ChannelType2,
94
+ logger as logger3,
95
+ stringToUuid as stringToUuid2
96
+ } from "@elizaos/core";
97
+ import { v4 as uuidv43 } from "uuid";
98
+
99
+ // src/heartbeat/delivery.ts
100
+ import {
101
+ logger
102
+ } from "@elizaos/core";
103
+ import { v4 as uuidv4 } from "uuid";
104
+ var LAST_ROUTE_COMPONENT_TYPE = "last_delivery_route";
105
+ var INTERNAL_SOURCES = /* @__PURE__ */ new Set(["cron", "webhook", "heartbeat", "internal"]);
106
+ async function readLastRoute(runtime) {
107
+ const component = await runtime.getComponent(runtime.agentId, LAST_ROUTE_COMPONENT_TYPE);
108
+ if (!component?.data) {
109
+ return null;
110
+ }
111
+ const data = component.data;
112
+ const source = typeof data.source === "string" ? data.source : "";
113
+ if (!source) {
114
+ return null;
115
+ }
116
+ return {
117
+ source,
118
+ channelId: typeof data.channelId === "string" ? data.channelId : void 0
119
+ };
120
+ }
121
+ async function writeLastRoute(runtime, target) {
122
+ const existing = await runtime.getComponent(runtime.agentId, LAST_ROUTE_COMPONENT_TYPE);
123
+ const data = {
124
+ source: target.source,
125
+ channelId: target.channelId ?? null,
126
+ updatedAt: Date.now()
127
+ };
128
+ if (existing) {
129
+ await runtime.updateComponent({
130
+ ...existing,
131
+ data
132
+ });
133
+ } else {
134
+ await runtime.createComponent({
135
+ id: uuidv4(),
136
+ entityId: runtime.agentId,
137
+ agentId: runtime.agentId,
138
+ roomId: runtime.agentId,
139
+ worldId: runtime.agentId,
140
+ sourceEntityId: runtime.agentId,
141
+ type: LAST_ROUTE_COMPONENT_TYPE,
142
+ data,
143
+ createdAt: Date.now()
144
+ });
145
+ }
146
+ }
147
+ async function scanRoomsForExternalTarget(runtime) {
148
+ const rooms = await runtime.getRooms(runtime.agentId).catch(() => []);
149
+ for (const room of rooms) {
150
+ if (room.source && !INTERNAL_SOURCES.has(room.source)) {
151
+ return {
152
+ source: room.source,
153
+ channelId: room.channelId ?? void 0
154
+ };
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ async function resolveDeliveryTarget(runtime, channel, to) {
160
+ if (channel !== "last") {
161
+ return { source: channel, channelId: to };
162
+ }
163
+ const stored = await readLastRoute(runtime);
164
+ if (stored) {
165
+ if (to) {
166
+ return { source: stored.source, channelId: to };
167
+ }
168
+ return stored;
169
+ }
170
+ const scanned = await scanRoomsForExternalTarget(runtime);
171
+ if (scanned) {
172
+ logger.debug(
173
+ `[Delivery] Resolved "last" via room scan: ${scanned.source}:${scanned.channelId ?? "(default)"}`
174
+ );
175
+ if (to) {
176
+ return { source: scanned.source, channelId: to };
177
+ }
178
+ return scanned;
179
+ }
180
+ return null;
181
+ }
182
+ async function deliverToTarget(runtime, content, channel, to, bestEffort) {
183
+ const target = await resolveDeliveryTarget(runtime, channel, to);
184
+ if (!target) {
185
+ const msg = `No delivery target resolved for channel "${channel}"`;
186
+ if (bestEffort) {
187
+ logger.warn(`[Delivery] ${msg}`);
188
+ return null;
189
+ }
190
+ throw new Error(msg);
191
+ }
192
+ const targetInfo = {
193
+ source: target.source
194
+ };
195
+ if (target.channelId) {
196
+ targetInfo.channelId = target.channelId;
197
+ }
198
+ const deliveryError = await runtime.sendMessageToTarget(targetInfo, content).then(
199
+ () => null,
200
+ (err) => err
201
+ );
202
+ if (deliveryError) {
203
+ if (bestEffort) {
204
+ logger.warn(
205
+ `[Delivery] Best-effort delivery failed to ${target.source}: ${deliveryError.message}`
206
+ );
207
+ return null;
208
+ }
209
+ throw deliveryError;
210
+ }
211
+ await writeLastRoute(runtime, target).catch((err) => {
212
+ logger.debug(`[Delivery] Failed to persist last route: ${err.message}`);
213
+ });
214
+ logger.info(
215
+ `[Delivery] Delivered to ${target.source}${target.channelId ? `:${target.channelId}` : ""}`
216
+ );
217
+ return target;
218
+ }
219
+
220
+ // src/heartbeat/queue.ts
221
+ var queues = /* @__PURE__ */ new Map();
222
+ function agentQueue(agentId) {
223
+ let q = queues.get(agentId);
224
+ if (!q) {
225
+ q = [];
226
+ queues.set(agentId, q);
227
+ }
228
+ return q;
229
+ }
230
+ function pushSystemEvent(agentId, text, source) {
231
+ agentQueue(agentId).push({ text, source, ts: Date.now() });
232
+ }
233
+ function drainSystemEvents(agentId) {
234
+ const q = queues.get(agentId);
235
+ if (!q || q.length === 0) {
236
+ return [];
237
+ }
238
+ const events = [...q];
239
+ q.length = 0;
240
+ return events;
241
+ }
242
+ function pendingEventCount(agentId) {
243
+ return queues.get(agentId)?.length ?? 0;
244
+ }
245
+
246
+ // src/heartbeat/worker.ts
247
+ import fs from "node:fs/promises";
248
+ import path from "node:path";
249
+ import {
250
+ ChannelType,
251
+ logger as logger2,
252
+ stringToUuid
253
+ } from "@elizaos/core";
254
+ import { v4 as uuidv42 } from "uuid";
255
+
256
+ // src/heartbeat/config.ts
257
+ var DEFAULT_EVERY_MS = 30 * 60 * 1e3;
258
+ var DEFAULT_PROMPT_FILE = "HEARTBEAT.md";
259
+ function parseDurationToMs(raw) {
260
+ const match = raw.match(/^(\d+(?:\.\d+)?)\s*(s|sec|m|min|h|hr|d|day|ms)?$/i);
261
+ if (!match) {
262
+ return DEFAULT_EVERY_MS;
263
+ }
264
+ const value = Number.parseFloat(match[1]);
265
+ const unit = (match[2] ?? "m").toLowerCase();
266
+ switch (unit) {
267
+ case "ms":
268
+ return value;
269
+ case "s":
270
+ case "sec":
271
+ return value * 1e3;
272
+ case "m":
273
+ case "min":
274
+ return value * 6e4;
275
+ case "h":
276
+ case "hr":
277
+ return value * 36e5;
278
+ case "d":
279
+ case "day":
280
+ return value * 864e5;
281
+ default:
282
+ return value * 6e4;
283
+ }
284
+ }
285
+ function parseActiveHours(raw) {
286
+ const start = typeof raw.start === "string" ? raw.start.trim() : "";
287
+ const end = typeof raw.end === "string" ? raw.end.trim() : "";
288
+ if (!start || !end) {
289
+ return null;
290
+ }
291
+ return { start, end };
292
+ }
293
+ function resolveHeartbeatConfig(runtime) {
294
+ const settings = runtime.character?.settings ?? {};
295
+ const hb = settings.heartbeat ?? {};
296
+ const everyRaw = typeof hb.every === "string" ? hb.every : "";
297
+ const everyMs = everyRaw ? parseDurationToMs(everyRaw) : DEFAULT_EVERY_MS;
298
+ const activeHours = hb.activeHours && typeof hb.activeHours === "object" ? parseActiveHours(hb.activeHours) : null;
299
+ const target = typeof hb.target === "string" ? hb.target.trim() : "last";
300
+ const promptFile = typeof hb.prompt === "string" && hb.prompt.trim() ? hb.prompt.trim() : DEFAULT_PROMPT_FILE;
301
+ const enabled = hb.enabled !== false;
302
+ return { everyMs, activeHours, target, promptFile, enabled };
303
+ }
304
+ function isWithinActiveHours(activeHours) {
305
+ if (!activeHours) {
306
+ return true;
307
+ }
308
+ const now = /* @__PURE__ */ new Date();
309
+ const hh = now.getHours();
310
+ const mm = now.getMinutes();
311
+ const currentMinutes = hh * 60 + mm;
312
+ const [startH, startM] = activeHours.start.split(":").map(Number);
313
+ const [endH, endM] = activeHours.end.split(":").map(Number);
314
+ const startMinutes = (startH ?? 0) * 60 + (startM ?? 0);
315
+ const endMinutes = (endH ?? 0) * 60 + (endM ?? 0);
316
+ if (startMinutes <= endMinutes) {
317
+ return currentMinutes >= startMinutes && currentMinutes < endMinutes;
318
+ }
319
+ return currentMinutes >= startMinutes || currentMinutes < endMinutes;
320
+ }
321
+
322
+ // src/heartbeat/worker.ts
323
+ var HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK";
324
+ var HEARTBEAT_ROOM_KEY = "heartbeat:main";
325
+ var HEARTBEAT_WORKER_NAME = "heartbeat";
326
+ async function readHeartbeatFile(runtime, filename) {
327
+ const settings = runtime.character?.settings ?? {};
328
+ const workspace = typeof settings.workspace === "string" ? settings.workspace : process.cwd();
329
+ const filePath = path.resolve(workspace, filename);
330
+ const content = await fs.readFile(filePath, "utf-8").catch(() => null);
331
+ return content;
332
+ }
333
+ function buildHeartbeatPrompt(heartbeatMd, events) {
334
+ const parts = [];
335
+ parts.push("[Heartbeat]");
336
+ if (heartbeatMd) {
337
+ parts.push("");
338
+ parts.push("# HEARTBEAT.md");
339
+ parts.push(heartbeatMd.trim());
340
+ parts.push("");
341
+ parts.push("Follow the instructions in HEARTBEAT.md strictly.");
342
+ }
343
+ if (events.length > 0) {
344
+ parts.push("");
345
+ parts.push("## System events since last heartbeat");
346
+ for (const ev of events) {
347
+ const age = Math.round((Date.now() - ev.ts) / 1e3);
348
+ parts.push(`- [${ev.source}, ${age}s ago] ${ev.text}`);
349
+ }
350
+ }
351
+ parts.push("");
352
+ parts.push(
353
+ 'If nothing requires your attention right now, reply with exactly "HEARTBEAT_OK" and nothing else.'
354
+ );
355
+ return parts.join("\n");
356
+ }
357
+ function isHeartbeatOk(text) {
358
+ const trimmed = text.trim();
359
+ return trimmed === HEARTBEAT_OK_TOKEN || trimmed.startsWith(`${HEARTBEAT_OK_TOKEN}
360
+ `) || trimmed.startsWith(`${HEARTBEAT_OK_TOKEN} `);
361
+ }
362
+ async function ensureHeartbeatRoom(runtime) {
363
+ const roomId = stringToUuid(`${runtime.agentId}-${HEARTBEAT_ROOM_KEY}`);
364
+ const existing = await runtime.getRoom(roomId);
365
+ if (!existing) {
366
+ await runtime.createRoom({
367
+ id: roomId,
368
+ name: "Heartbeat",
369
+ source: "cron",
370
+ type: ChannelType.GROUP,
371
+ channelId: HEARTBEAT_ROOM_KEY
372
+ });
373
+ await runtime.addParticipant(runtime.agentId, roomId);
374
+ }
375
+ return roomId;
376
+ }
377
+ async function runHeartbeatTick(runtime, config) {
378
+ if (!isWithinActiveHours(config.activeHours)) {
379
+ logger2.debug("[Heartbeat] Outside active hours, skipping");
380
+ return;
381
+ }
382
+ const events = drainSystemEvents(runtime.agentId);
383
+ const heartbeatMd = await readHeartbeatFile(runtime, config.promptFile);
384
+ if (!heartbeatMd && events.length === 0) {
385
+ logger2.debug("[Heartbeat] No HEARTBEAT.md and no pending events, skipping");
386
+ return;
387
+ }
388
+ const promptText = buildHeartbeatPrompt(heartbeatMd, events);
389
+ const roomId = await ensureHeartbeatRoom(runtime);
390
+ const messageId = uuidv42();
391
+ const memory = {
392
+ id: messageId,
393
+ entityId: runtime.agentId,
394
+ roomId,
395
+ agentId: runtime.agentId,
396
+ content: { text: promptText },
397
+ createdAt: Date.now()
398
+ };
399
+ let responseText = "";
400
+ const callback = async (response) => {
401
+ if (response.text) {
402
+ responseText += response.text;
403
+ }
404
+ return [];
405
+ };
406
+ logger2.info(
407
+ `[Heartbeat] Running tick (${events.length} pending events, HEARTBEAT.md: ${heartbeatMd ? "yes" : "no"})`
408
+ );
409
+ if (!runtime.messageService) {
410
+ throw new Error("messageService is not available on runtime");
411
+ }
412
+ await runtime.messageService.handleMessage(runtime, memory, callback);
413
+ if (!responseText.trim() || isHeartbeatOk(responseText)) {
414
+ logger2.debug("[Heartbeat] Agent responded HEARTBEAT_OK, suppressing delivery");
415
+ return;
416
+ }
417
+ logger2.info(`[Heartbeat] Agent has something to say, delivering to target "${config.target}"`);
418
+ if (config.target === "none") {
419
+ return;
420
+ }
421
+ let channel;
422
+ let to;
423
+ if (config.target === "last" || config.target === "none") {
424
+ channel = config.target;
425
+ } else {
426
+ const colonIdx = config.target.indexOf(":");
427
+ if (colonIdx === -1) {
428
+ channel = config.target;
429
+ } else {
430
+ channel = config.target.slice(0, colonIdx);
431
+ to = config.target.slice(colonIdx + 1) || void 0;
432
+ }
433
+ }
434
+ if (channel === "none") {
435
+ return;
436
+ }
437
+ await deliverToTarget(
438
+ runtime,
439
+ { text: responseText },
440
+ channel,
441
+ to,
442
+ true
443
+ // bestEffort for heartbeat -- don't crash the tick on delivery failure
444
+ );
445
+ }
446
+ var heartbeatWorker = {
447
+ name: HEARTBEAT_WORKER_NAME,
448
+ async execute(runtime, _options, _task) {
449
+ const config = resolveHeartbeatConfig(runtime);
450
+ if (!config.enabled) {
451
+ return;
452
+ }
453
+ await runHeartbeatTick(runtime, config);
454
+ }
455
+ };
456
+ async function startHeartbeat(runtime) {
457
+ const config = resolveHeartbeatConfig(runtime);
458
+ if (!config.enabled) {
459
+ logger2.info("[Heartbeat] Disabled via config");
460
+ return;
461
+ }
462
+ runtime.registerTaskWorker(heartbeatWorker);
463
+ const existingTasks = await runtime.getTasks({
464
+ roomId: runtime.agentId,
465
+ tags: ["heartbeat", "queue", "repeat"]
466
+ });
467
+ const alreadyExists = existingTasks.some((t) => t.name === HEARTBEAT_WORKER_NAME);
468
+ if (!alreadyExists) {
469
+ await runtime.createTask({
470
+ name: HEARTBEAT_WORKER_NAME,
471
+ description: "Periodic agent heartbeat \u2013 reads HEARTBEAT.md and checks system events",
472
+ roomId: runtime.agentId,
473
+ tags: ["heartbeat", "queue", "repeat"],
474
+ metadata: {
475
+ updateInterval: config.everyMs,
476
+ updatedAt: Date.now(),
477
+ blocking: true
478
+ }
479
+ });
480
+ logger2.info(`[Heartbeat] Created recurring task (every ${Math.round(config.everyMs / 1e3)}s)`);
481
+ } else {
482
+ logger2.info("[Heartbeat] Recurring task already exists");
483
+ }
484
+ }
485
+ async function wakeHeartbeatNow(runtime) {
486
+ runtime.registerTaskWorker(heartbeatWorker);
487
+ await runtime.createTask({
488
+ name: HEARTBEAT_WORKER_NAME,
489
+ description: "Immediate heartbeat wake",
490
+ roomId: runtime.agentId,
491
+ tags: ["heartbeat", "queue"],
492
+ metadata: {
493
+ updatedAt: 0,
494
+ // ensures it runs immediately
495
+ blocking: false
496
+ }
497
+ });
498
+ logger2.info("[Heartbeat] Queued immediate wake");
499
+ }
500
+
501
+ // src/types.ts
502
+ var DEFAULT_CRON_CONFIG = {
503
+ minIntervalMs: 1e4,
504
+ // 10 seconds minimum
505
+ maxJobsPerAgent: 100,
506
+ defaultTimeoutMs: 3e5,
507
+ // 5 minutes
508
+ catchUpMissedJobs: false,
509
+ catchUpWindowMs: 36e5,
510
+ // 1 hour
511
+ timerCheckIntervalMs: 1e3
512
+ // 1 second
513
+ };
514
+
515
+ // src/otto/executor.ts
516
+ function withTimeout(promise, timeoutMs) {
517
+ let timer;
518
+ const timeout = new Promise((_, reject) => {
519
+ timer = setTimeout(() => reject(new Error("Job execution timeout")), timeoutMs);
520
+ });
521
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
522
+ }
523
+ async function executeSystemEvent(runtime, job, payload) {
524
+ const startedAtMs = Date.now();
525
+ pushSystemEvent(runtime.agentId, payload.text, `cron:${job.id}`);
526
+ logger3.info(`[Otto Executor] Queued system event for heartbeat: "${payload.text.slice(0, 80)}"`);
527
+ if (job.wakeMode === "now") {
528
+ await wakeHeartbeatNow(runtime);
529
+ }
530
+ return {
531
+ status: "ok",
532
+ durationMs: Date.now() - startedAtMs,
533
+ output: `System event queued (wake: ${job.wakeMode})`
534
+ };
535
+ }
536
+ async function ensureCronRoom(runtime, jobId, jobName) {
537
+ const roomKey = `cron:${jobId}`;
538
+ const roomId = stringToUuid2(`${runtime.agentId}-${roomKey}`);
539
+ const existing = await runtime.getRoom(roomId);
540
+ if (!existing) {
541
+ await runtime.createRoom({
542
+ id: roomId,
543
+ name: `Cron: ${jobName}`,
544
+ source: "cron",
545
+ type: ChannelType2.GROUP,
546
+ channelId: roomKey
547
+ });
548
+ await runtime.addParticipant(runtime.agentId, roomId);
549
+ }
550
+ return roomId;
551
+ }
552
+ async function executeAgentTurn(runtime, job, payload, config) {
553
+ const startedAtMs = Date.now();
554
+ const timeoutMs = payload.timeoutSeconds ? payload.timeoutSeconds * 1e3 : config.defaultTimeoutMs ?? DEFAULT_CRON_CONFIG.defaultTimeoutMs;
555
+ const roomId = await ensureCronRoom(runtime, job.id, job.name);
556
+ const promptPrefix = `[cron:${job.id} ${job.name}]`;
557
+ const messageText = `${promptPrefix} ${payload.message}`;
558
+ const messageId = uuidv43();
559
+ const memory = {
560
+ id: messageId,
561
+ entityId: runtime.agentId,
562
+ roomId,
563
+ agentId: runtime.agentId,
564
+ content: { text: messageText },
565
+ createdAt: Date.now()
566
+ };
567
+ let responseText = "";
568
+ const callback = async (response) => {
569
+ if (response.text) {
570
+ responseText += response.text;
571
+ }
572
+ return [];
573
+ };
574
+ let status = "ok";
575
+ let error;
576
+ if (!runtime.messageService) {
577
+ throw new Error("messageService is not available on runtime");
578
+ }
579
+ const runPromise = runtime.messageService.handleMessage(runtime, memory, callback);
580
+ await withTimeout(runPromise, timeoutMs).catch((err) => {
581
+ if (err.message === "Job execution timeout") {
582
+ status = "timeout";
583
+ error = "Execution timed out";
584
+ } else {
585
+ status = "error";
586
+ error = err.message;
587
+ }
588
+ });
589
+ const durationMs = Date.now() - startedAtMs;
590
+ if (status !== "ok") {
591
+ return { status, durationMs, error };
592
+ }
593
+ const plan = resolveCronDeliveryPlan(job);
594
+ if (plan.requested && responseText.trim() && responseText.trim() !== "HEARTBEAT_OK") {
595
+ logger3.info(
596
+ `[Otto Executor] Delivering response for "${job.name}" to ${plan.channel}${plan.to ? `:${plan.to}` : ""}`
597
+ );
598
+ const deliveryError = await deliverToTarget(
599
+ runtime,
600
+ { text: responseText },
601
+ plan.channel,
602
+ plan.to,
603
+ job.delivery?.bestEffort
604
+ ).then(
605
+ () => null,
606
+ (err) => err
607
+ );
608
+ if (deliveryError) {
609
+ return {
610
+ status: "error",
611
+ durationMs: Date.now() - startedAtMs,
612
+ output: responseText,
613
+ error: `Delivery failed: ${deliveryError.message}`
614
+ };
615
+ }
616
+ }
617
+ if (responseText.trim() && responseText.trim() !== "HEARTBEAT_OK") {
618
+ const summary = responseText.length > 200 ? `${responseText.slice(0, 200)}\u2026` : responseText;
619
+ pushSystemEvent(
620
+ runtime.agentId,
621
+ `[Cron "${job.name}" completed] ${summary}`,
622
+ `cron:${job.id}`
623
+ );
624
+ if (job.wakeMode === "now") {
625
+ await wakeHeartbeatNow(runtime);
626
+ }
627
+ }
628
+ return {
629
+ status: "ok",
630
+ durationMs,
631
+ output: responseText || void 0
632
+ };
633
+ }
634
+ async function executeOttoJob(runtime, job, config) {
635
+ const { payload } = job;
636
+ switch (payload.kind) {
637
+ case "systemEvent":
638
+ return executeSystemEvent(runtime, job, payload);
639
+ case "agentTurn":
640
+ return executeAgentTurn(runtime, job, payload, config);
641
+ default: {
642
+ const kind = payload.kind;
643
+ return {
644
+ status: "error",
645
+ durationMs: 0,
646
+ error: `Unknown Otto payload kind: ${kind}`
647
+ };
648
+ }
649
+ }
650
+ }
651
+
652
+ // src/otto/job-utils.ts
653
+ function assertSupportedJobSpec(job) {
654
+ if (job.sessionTarget === "main" && job.payload.kind !== "systemEvent") {
655
+ throw new Error('main cron jobs require payload.kind="systemEvent"');
656
+ }
657
+ if (job.sessionTarget === "isolated" && job.payload.kind !== "agentTurn") {
658
+ throw new Error('isolated cron jobs require payload.kind="agentTurn"');
659
+ }
660
+ }
661
+ function assertDeliverySupport(job) {
662
+ if (job.delivery && job.sessionTarget !== "isolated") {
663
+ throw new Error('cron delivery config is only supported for sessionTarget="isolated"');
664
+ }
665
+ }
666
+ function normalizeRequiredName(name) {
667
+ const val = typeof name === "string" ? name.trim() : "";
668
+ if (!val) {
669
+ throw new Error("cron job name is required");
670
+ }
671
+ return val;
672
+ }
673
+ function normalizeOptionalText(text) {
674
+ if (typeof text !== "string") {
675
+ return void 0;
676
+ }
677
+ const trimmed = text.trim();
678
+ return trimmed || void 0;
679
+ }
680
+ function normalizeOptionalAgentId(agentId) {
681
+ if (typeof agentId !== "string") {
682
+ return void 0;
683
+ }
684
+ const trimmed = agentId.trim().toLowerCase();
685
+ return trimmed || void 0;
686
+ }
687
+ function applyJobPatch(job, patch) {
688
+ if ("name" in patch) {
689
+ job.name = normalizeRequiredName(patch.name);
690
+ }
691
+ if ("description" in patch) {
692
+ job.description = normalizeOptionalText(patch.description);
693
+ }
694
+ if (typeof patch.enabled === "boolean") {
695
+ job.enabled = patch.enabled;
696
+ }
697
+ if (typeof patch.deleteAfterRun === "boolean") {
698
+ job.deleteAfterRun = patch.deleteAfterRun;
699
+ }
700
+ if (patch.schedule) {
701
+ job.schedule = patch.schedule;
702
+ }
703
+ if (patch.sessionTarget) {
704
+ job.sessionTarget = patch.sessionTarget;
705
+ }
706
+ if (patch.wakeMode) {
707
+ job.wakeMode = patch.wakeMode;
708
+ }
709
+ if (patch.payload) {
710
+ job.payload = mergeCronPayload(job.payload, patch.payload);
711
+ }
712
+ if (!patch.delivery && patch.payload?.kind === "agentTurn") {
713
+ const legacyDeliveryPatch = buildLegacyDeliveryPatch(patch.payload);
714
+ if (legacyDeliveryPatch && job.sessionTarget === "isolated" && job.payload.kind === "agentTurn") {
715
+ job.delivery = mergeCronDelivery(job.delivery, legacyDeliveryPatch);
716
+ }
717
+ }
718
+ if (patch.delivery) {
719
+ job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
720
+ }
721
+ if (job.sessionTarget === "main" && job.delivery) {
722
+ job.delivery = void 0;
723
+ }
724
+ if (patch.state) {
725
+ job.state = { ...job.state, ...patch.state };
726
+ }
727
+ if ("agentId" in patch) {
728
+ job.agentId = normalizeOptionalAgentId(patch.agentId);
729
+ }
730
+ assertSupportedJobSpec(job);
731
+ assertDeliverySupport(job);
732
+ }
733
+ function mergeCronPayload(existing, patch) {
734
+ if (patch.kind !== existing.kind) {
735
+ return buildPayloadFromPatch(patch);
736
+ }
737
+ if (patch.kind === "systemEvent") {
738
+ if (existing.kind !== "systemEvent") {
739
+ return buildPayloadFromPatch(patch);
740
+ }
741
+ const text = typeof patch.text === "string" ? patch.text : existing.text;
742
+ return { kind: "systemEvent", text };
743
+ }
744
+ if (existing.kind !== "agentTurn") {
745
+ return buildPayloadFromPatch(patch);
746
+ }
747
+ const next = { ...existing };
748
+ if (typeof patch.message === "string") {
749
+ next.message = patch.message;
750
+ }
751
+ if (typeof patch.model === "string") {
752
+ next.model = patch.model;
753
+ }
754
+ if (typeof patch.thinking === "string") {
755
+ next.thinking = patch.thinking;
756
+ }
757
+ if (typeof patch.timeoutSeconds === "number") {
758
+ next.timeoutSeconds = patch.timeoutSeconds;
759
+ }
760
+ if (typeof patch.deliver === "boolean") {
761
+ next.deliver = patch.deliver;
762
+ }
763
+ if (typeof patch.channel === "string") {
764
+ next.channel = patch.channel;
765
+ }
766
+ if (typeof patch.to === "string") {
767
+ next.to = patch.to;
768
+ }
769
+ if (typeof patch.bestEffortDeliver === "boolean") {
770
+ next.bestEffortDeliver = patch.bestEffortDeliver;
771
+ }
772
+ return next;
773
+ }
774
+ function buildLegacyDeliveryPatch(payload) {
775
+ const deliver = payload.deliver;
776
+ const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
777
+ const hasLegacyHints = typeof deliver === "boolean" || typeof payload.bestEffortDeliver === "boolean" || Boolean(toRaw);
778
+ if (!hasLegacyHints) {
779
+ return null;
780
+ }
781
+ const patch = {};
782
+ let hasPatch = false;
783
+ if (deliver === false) {
784
+ patch.mode = "none";
785
+ hasPatch = true;
786
+ } else if (deliver === true || toRaw) {
787
+ patch.mode = "announce";
788
+ hasPatch = true;
789
+ }
790
+ if (typeof payload.channel === "string") {
791
+ const channel = payload.channel.trim().toLowerCase();
792
+ patch.channel = channel ? channel : void 0;
793
+ hasPatch = true;
794
+ }
795
+ if (typeof payload.to === "string") {
796
+ patch.to = payload.to.trim();
797
+ hasPatch = true;
798
+ }
799
+ if (typeof payload.bestEffortDeliver === "boolean") {
800
+ patch.bestEffort = payload.bestEffortDeliver;
801
+ hasPatch = true;
802
+ }
803
+ return hasPatch ? patch : null;
804
+ }
805
+ function buildPayloadFromPatch(patch) {
806
+ if (patch.kind === "systemEvent") {
807
+ if (typeof patch.text !== "string" || patch.text.length === 0) {
808
+ throw new Error('cron.update payload.kind="systemEvent" requires text');
809
+ }
810
+ return { kind: "systemEvent", text: patch.text };
811
+ }
812
+ if (typeof patch.message !== "string" || patch.message.length === 0) {
813
+ throw new Error('cron.update payload.kind="agentTurn" requires message');
814
+ }
815
+ return {
816
+ kind: "agentTurn",
817
+ message: patch.message,
818
+ model: patch.model,
819
+ thinking: patch.thinking,
820
+ timeoutSeconds: patch.timeoutSeconds,
821
+ deliver: patch.deliver,
822
+ channel: patch.channel,
823
+ to: patch.to,
824
+ bestEffortDeliver: patch.bestEffortDeliver
825
+ };
826
+ }
827
+ function mergeCronDelivery(existing, patch) {
828
+ const next = {
829
+ mode: existing?.mode ?? "none",
830
+ channel: existing?.channel,
831
+ to: existing?.to,
832
+ bestEffort: existing?.bestEffort
833
+ };
834
+ if (typeof patch.mode === "string") {
835
+ next.mode = patch.mode === "deliver" ? "announce" : patch.mode;
836
+ }
837
+ if ("channel" in patch) {
838
+ const channel = typeof patch.channel === "string" ? patch.channel.trim() : "";
839
+ next.channel = channel ? channel : void 0;
840
+ }
841
+ if ("to" in patch) {
842
+ const to = typeof patch.to === "string" ? patch.to.trim() : "";
843
+ next.to = to ? to : void 0;
844
+ }
845
+ if (typeof patch.bestEffort === "boolean") {
846
+ next.bestEffort = patch.bestEffort;
847
+ }
848
+ return next;
849
+ }
850
+
851
+ // src/otto/parse.ts
852
+ var ISO_TZ_RE = /(Z|[+-]\d{2}:?\d{2})$/i;
853
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
854
+ var ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T/;
855
+ function normalizeUtcIso(raw) {
856
+ if (ISO_TZ_RE.test(raw)) {
857
+ return raw;
858
+ }
859
+ if (ISO_DATE_RE.test(raw)) {
860
+ return `${raw}T00:00:00Z`;
861
+ }
862
+ if (ISO_DATE_TIME_RE.test(raw)) {
863
+ return `${raw}Z`;
864
+ }
865
+ return raw;
866
+ }
867
+ function parseAbsoluteTimeMs(input) {
868
+ const raw = input.trim();
869
+ if (!raw) {
870
+ return null;
871
+ }
872
+ if (/^\d+$/.test(raw)) {
873
+ const n = Number(raw);
874
+ if (Number.isFinite(n) && n > 0) {
875
+ return Math.floor(n);
876
+ }
877
+ }
878
+ const parsed = Date.parse(normalizeUtcIso(raw));
879
+ return Number.isFinite(parsed) ? parsed : null;
880
+ }
881
+
882
+ // src/otto/payload-migration.ts
883
+ function readString(value) {
884
+ if (typeof value !== "string") {
885
+ return void 0;
886
+ }
887
+ return value;
888
+ }
889
+ function normalizeChannel2(value) {
890
+ return value.trim().toLowerCase();
891
+ }
892
+ function migrateLegacyCronPayload(payload) {
893
+ let mutated = false;
894
+ const channelValue = readString(payload.channel);
895
+ const providerValue = readString(payload.provider);
896
+ const nextChannel = typeof channelValue === "string" && channelValue.trim().length > 0 ? normalizeChannel2(channelValue) : typeof providerValue === "string" && providerValue.trim().length > 0 ? normalizeChannel2(providerValue) : "";
897
+ if (nextChannel) {
898
+ if (channelValue !== nextChannel) {
899
+ payload.channel = nextChannel;
900
+ mutated = true;
901
+ }
902
+ }
903
+ if ("provider" in payload) {
904
+ delete payload.provider;
905
+ mutated = true;
906
+ }
907
+ return mutated;
908
+ }
909
+
910
+ // src/otto/normalize.ts
911
+ var DEFAULT_OPTIONS = {
912
+ applyDefaults: false
913
+ };
914
+ function isRecord(value) {
915
+ return typeof value === "object" && value !== null && !Array.isArray(value);
916
+ }
917
+ function coerceSchedule(schedule) {
918
+ const next = { ...schedule };
919
+ const kind = typeof schedule.kind === "string" ? schedule.kind : void 0;
920
+ const atMsRaw = schedule.atMs;
921
+ const atRaw = schedule.at;
922
+ const atString = typeof atRaw === "string" ? atRaw.trim() : "";
923
+ const parsedAtMs = typeof atMsRaw === "number" ? atMsRaw : typeof atMsRaw === "string" ? parseAbsoluteTimeMs(atMsRaw) : atString ? parseAbsoluteTimeMs(atString) : null;
924
+ if (!kind) {
925
+ if (typeof schedule.atMs === "number" || typeof schedule.at === "string" || typeof schedule.atMs === "string") {
926
+ next.kind = "at";
927
+ } else if (typeof schedule.everyMs === "number") {
928
+ next.kind = "every";
929
+ } else if (typeof schedule.expr === "string") {
930
+ next.kind = "cron";
931
+ }
932
+ }
933
+ if (atString) {
934
+ next.at = parsedAtMs ? new Date(parsedAtMs).toISOString() : atString;
935
+ } else if (parsedAtMs !== null) {
936
+ next.at = new Date(parsedAtMs).toISOString();
937
+ }
938
+ if ("atMs" in next) {
939
+ delete next.atMs;
940
+ }
941
+ return next;
942
+ }
943
+ function coercePayload(payload) {
944
+ const next = { ...payload };
945
+ migrateLegacyCronPayload(next);
946
+ return next;
947
+ }
948
+ function coerceDelivery(delivery) {
949
+ const next = { ...delivery };
950
+ if (typeof delivery.mode === "string") {
951
+ const mode = delivery.mode.trim().toLowerCase();
952
+ next.mode = mode === "deliver" ? "announce" : mode;
953
+ }
954
+ if (typeof delivery.channel === "string") {
955
+ const trimmed = delivery.channel.trim().toLowerCase();
956
+ if (trimmed) {
957
+ next.channel = trimmed;
958
+ } else {
959
+ delete next.channel;
960
+ }
961
+ }
962
+ if (typeof delivery.to === "string") {
963
+ const trimmed = delivery.to.trim();
964
+ if (trimmed) {
965
+ next.to = trimmed;
966
+ } else {
967
+ delete next.to;
968
+ }
969
+ }
970
+ return next;
971
+ }
972
+ function hasLegacyDeliveryHints(payload) {
973
+ if (typeof payload.deliver === "boolean") {
974
+ return true;
975
+ }
976
+ if (typeof payload.bestEffortDeliver === "boolean") {
977
+ return true;
978
+ }
979
+ if (typeof payload.to === "string" && payload.to.trim()) {
980
+ return true;
981
+ }
982
+ return false;
983
+ }
984
+ function buildDeliveryFromLegacyPayload(payload) {
985
+ const deliver = payload.deliver;
986
+ const mode = deliver === false ? "none" : "announce";
987
+ const channelRaw = typeof payload.channel === "string" ? payload.channel.trim().toLowerCase() : "";
988
+ const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
989
+ const next = { mode };
990
+ if (channelRaw) {
991
+ next.channel = channelRaw;
992
+ }
993
+ if (toRaw) {
994
+ next.to = toRaw;
995
+ }
996
+ if (typeof payload.bestEffortDeliver === "boolean") {
997
+ next.bestEffort = payload.bestEffortDeliver;
998
+ }
999
+ return next;
1000
+ }
1001
+ function stripLegacyDeliveryFields(payload) {
1002
+ if ("deliver" in payload) {
1003
+ delete payload.deliver;
1004
+ }
1005
+ if ("channel" in payload) {
1006
+ delete payload.channel;
1007
+ }
1008
+ if ("to" in payload) {
1009
+ delete payload.to;
1010
+ }
1011
+ if ("bestEffortDeliver" in payload) {
1012
+ delete payload.bestEffortDeliver;
1013
+ }
1014
+ }
1015
+ function unwrapJob(raw) {
1016
+ if (isRecord(raw.data)) {
1017
+ return raw.data;
1018
+ }
1019
+ if (isRecord(raw.job)) {
1020
+ return raw.job;
1021
+ }
1022
+ return raw;
1023
+ }
1024
+ function defaultSanitizeAgentId(raw) {
1025
+ return raw.trim().toLowerCase();
1026
+ }
1027
+ function normalizeCronJobInput(raw, options = DEFAULT_OPTIONS) {
1028
+ if (!isRecord(raw)) {
1029
+ return null;
1030
+ }
1031
+ const base = unwrapJob(raw);
1032
+ const next = { ...base };
1033
+ const sanitizeAgentId = options.sanitizeAgentId ?? defaultSanitizeAgentId;
1034
+ if ("agentId" in base) {
1035
+ const agentId = base.agentId;
1036
+ if (agentId === null) {
1037
+ next.agentId = null;
1038
+ } else if (typeof agentId === "string") {
1039
+ const trimmed = agentId.trim();
1040
+ if (trimmed) {
1041
+ next.agentId = sanitizeAgentId(trimmed);
1042
+ } else {
1043
+ delete next.agentId;
1044
+ }
1045
+ }
1046
+ }
1047
+ if ("enabled" in base) {
1048
+ const enabled = base.enabled;
1049
+ if (typeof enabled === "boolean") {
1050
+ next.enabled = enabled;
1051
+ } else if (typeof enabled === "string") {
1052
+ const trimmed = enabled.trim().toLowerCase();
1053
+ if (trimmed === "true") {
1054
+ next.enabled = true;
1055
+ }
1056
+ if (trimmed === "false") {
1057
+ next.enabled = false;
1058
+ }
1059
+ }
1060
+ }
1061
+ if (isRecord(base.schedule)) {
1062
+ next.schedule = coerceSchedule(base.schedule);
1063
+ }
1064
+ if (isRecord(base.payload)) {
1065
+ next.payload = coercePayload(base.payload);
1066
+ }
1067
+ if (isRecord(base.delivery)) {
1068
+ next.delivery = coerceDelivery(base.delivery);
1069
+ }
1070
+ if (isRecord(base.isolation)) {
1071
+ delete next.isolation;
1072
+ }
1073
+ if (options.applyDefaults) {
1074
+ if (!next.wakeMode) {
1075
+ next.wakeMode = "next-heartbeat";
1076
+ }
1077
+ if (typeof next.enabled !== "boolean") {
1078
+ next.enabled = true;
1079
+ }
1080
+ if (!next.sessionTarget && isRecord(next.payload)) {
1081
+ const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
1082
+ if (kind === "systemEvent") {
1083
+ next.sessionTarget = "main";
1084
+ }
1085
+ if (kind === "agentTurn") {
1086
+ next.sessionTarget = "isolated";
1087
+ }
1088
+ }
1089
+ if ("schedule" in next && isRecord(next.schedule) && next.schedule.kind === "at" && !("deleteAfterRun" in next)) {
1090
+ next.deleteAfterRun = true;
1091
+ }
1092
+ const payload = isRecord(next.payload) ? next.payload : null;
1093
+ const payloadKind = payload && typeof payload.kind === "string" ? payload.kind : "";
1094
+ const sessionTarget = typeof next.sessionTarget === "string" ? next.sessionTarget : "";
1095
+ const isIsolatedAgentTurn = sessionTarget === "isolated" || sessionTarget === "" && payloadKind === "agentTurn";
1096
+ const hasDelivery = "delivery" in next && next.delivery !== void 0;
1097
+ const hasLegacyDelivery = payload ? hasLegacyDeliveryHints(payload) : false;
1098
+ if (!hasDelivery && isIsolatedAgentTurn && payloadKind === "agentTurn") {
1099
+ if (payload && hasLegacyDelivery) {
1100
+ next.delivery = buildDeliveryFromLegacyPayload(payload);
1101
+ stripLegacyDeliveryFields(payload);
1102
+ } else {
1103
+ next.delivery = { mode: "announce" };
1104
+ }
1105
+ }
1106
+ }
1107
+ return next;
1108
+ }
1109
+ function normalizeCronJobCreate(raw, options) {
1110
+ return normalizeCronJobInput(raw, {
1111
+ applyDefaults: true,
1112
+ ...options
1113
+ });
1114
+ }
1115
+ function normalizeCronJobPatch(raw, options) {
1116
+ return normalizeCronJobInput(raw, {
1117
+ applyDefaults: false,
1118
+ ...options
1119
+ });
1120
+ }
1121
+
1122
+ // src/otto/run-log.ts
1123
+ import fs2 from "node:fs/promises";
1124
+ import path2 from "node:path";
1125
+ function resolveCronRunLogPath(params) {
1126
+ const storePath = path2.resolve(params.storePath);
1127
+ const dir = path2.dirname(storePath);
1128
+ return path2.join(dir, "runs", `${params.jobId}.jsonl`);
1129
+ }
1130
+ var writesByPath = /* @__PURE__ */ new Map();
1131
+ async function pruneIfNeeded(filePath, opts) {
1132
+ const stat = await fs2.stat(filePath).catch(() => null);
1133
+ if (!stat || stat.size <= opts.maxBytes) {
1134
+ return;
1135
+ }
1136
+ const raw = await fs2.readFile(filePath, "utf-8").catch(() => "");
1137
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
1138
+ const kept = lines.slice(Math.max(0, lines.length - opts.keepLines));
1139
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
1140
+ await fs2.writeFile(tmp, `${kept.join("\n")}
1141
+ `, "utf-8");
1142
+ await fs2.rename(tmp, filePath);
1143
+ }
1144
+ async function appendCronRunLog(filePath, entry, opts) {
1145
+ const resolved = path2.resolve(filePath);
1146
+ const prev = writesByPath.get(resolved) ?? Promise.resolve();
1147
+ const next = prev.catch(() => void 0).then(async () => {
1148
+ await fs2.mkdir(path2.dirname(resolved), { recursive: true });
1149
+ await fs2.appendFile(resolved, `${JSON.stringify(entry)}
1150
+ `, "utf-8");
1151
+ await pruneIfNeeded(resolved, {
1152
+ maxBytes: opts?.maxBytes ?? 2e6,
1153
+ keepLines: opts?.keepLines ?? 2e3
1154
+ });
1155
+ });
1156
+ writesByPath.set(resolved, next);
1157
+ await next;
1158
+ }
1159
+ async function readCronRunLogEntries(filePath, opts) {
1160
+ const limit = Math.max(1, Math.min(5e3, Math.floor(opts?.limit ?? 200)));
1161
+ const jobId = opts?.jobId?.trim() || void 0;
1162
+ const raw = await fs2.readFile(path2.resolve(filePath), "utf-8").catch(() => "");
1163
+ if (!raw.trim()) {
1164
+ return [];
1165
+ }
1166
+ const parsed = [];
1167
+ const lines = raw.split("\n");
1168
+ for (let i = lines.length - 1; i >= 0 && parsed.length < limit; i--) {
1169
+ const line = lines[i]?.trim();
1170
+ if (!line) {
1171
+ continue;
1172
+ }
1173
+ try {
1174
+ const obj = JSON.parse(line);
1175
+ if (!obj || typeof obj !== "object") {
1176
+ continue;
1177
+ }
1178
+ if (obj.action !== "finished") {
1179
+ continue;
1180
+ }
1181
+ if (typeof obj.jobId !== "string" || obj.jobId.trim().length === 0) {
1182
+ continue;
1183
+ }
1184
+ if (typeof obj.ts !== "number" || !Number.isFinite(obj.ts)) {
1185
+ continue;
1186
+ }
1187
+ if (jobId && obj.jobId !== jobId) {
1188
+ continue;
1189
+ }
1190
+ parsed.push(obj);
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ return [...parsed].reverse();
1195
+ }
1196
+
1197
+ // src/otto/store.ts
1198
+ import fs3 from "node:fs";
1199
+ import os from "node:os";
1200
+ import path3 from "node:path";
1201
+ function resolveCronStorePath(storePath) {
1202
+ if (storePath?.trim()) {
1203
+ const raw = storePath.trim();
1204
+ if (raw.startsWith("~")) {
1205
+ return path3.resolve(raw.replace("~", os.homedir()));
1206
+ }
1207
+ return path3.resolve(raw);
1208
+ }
1209
+ return void 0;
1210
+ }
1211
+ async function loadCronStore(storePath) {
1212
+ try {
1213
+ const raw = await fs3.promises.readFile(storePath, "utf-8");
1214
+ let parsed;
1215
+ try {
1216
+ const JSON5 = await import("./dist-KM2GC6Y3.js").then((m) => m.default);
1217
+ parsed = JSON5.parse(raw);
1218
+ } catch {
1219
+ parsed = JSON.parse(raw);
1220
+ }
1221
+ const jobs = Array.isArray(parsed?.jobs) ? parsed?.jobs : [];
1222
+ return {
1223
+ version: 1,
1224
+ jobs: jobs.filter(Boolean)
1225
+ };
1226
+ } catch {
1227
+ return { version: 1, jobs: [] };
1228
+ }
1229
+ }
1230
+ async function saveCronStore(storePath, store) {
1231
+ await fs3.promises.mkdir(path3.dirname(storePath), { recursive: true });
1232
+ const tmp = `${storePath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
1233
+ const json = JSON.stringify(store, null, 2);
1234
+ await fs3.promises.writeFile(tmp, json, "utf-8");
1235
+ await fs3.promises.rename(tmp, storePath);
1236
+ try {
1237
+ await fs3.promises.copyFile(storePath, `${storePath}.bak`);
1238
+ } catch {
1239
+ }
1240
+ }
1241
+
1242
+ // src/otto/validate-timestamp.ts
1243
+ var ONE_MINUTE_MS = 60 * 1e3;
1244
+ var TEN_YEARS_MS = 10 * 365.25 * 24 * 60 * 60 * 1e3;
1245
+ function validateScheduleTimestamp(schedule, nowMs = Date.now()) {
1246
+ if (schedule.kind !== "at") {
1247
+ return { ok: true };
1248
+ }
1249
+ const atRaw = typeof schedule.at === "string" ? schedule.at.trim() : "";
1250
+ const atMs = atRaw ? parseAbsoluteTimeMs(atRaw) : null;
1251
+ if (atMs === null || !Number.isFinite(atMs)) {
1252
+ return {
1253
+ ok: false,
1254
+ message: `Invalid schedule.at: expected ISO-8601 timestamp (got ${String(schedule.at)})`
1255
+ };
1256
+ }
1257
+ const diffMs = atMs - nowMs;
1258
+ if (diffMs < -ONE_MINUTE_MS) {
1259
+ const nowDate = new Date(nowMs).toISOString();
1260
+ const atDate = new Date(atMs).toISOString();
1261
+ const minutesAgo = Math.floor(-diffMs / ONE_MINUTE_MS);
1262
+ return {
1263
+ ok: false,
1264
+ message: `schedule.at is in the past: ${atDate} (${minutesAgo} minutes ago). Current time: ${nowDate}`
1265
+ };
1266
+ }
1267
+ if (diffMs > TEN_YEARS_MS) {
1268
+ const atDate = new Date(atMs).toISOString();
1269
+ const yearsAhead = Math.floor(diffMs / (365.25 * 24 * 60 * 60 * 1e3));
1270
+ return {
1271
+ ok: false,
1272
+ message: `schedule.at is too far in the future: ${atDate} (${yearsAhead} years ahead). Maximum allowed: 10 years`
1273
+ };
1274
+ }
1275
+ return { ok: true };
1276
+ }
1277
+
1278
+ export {
1279
+ DEFAULT_CRON_CONFIG,
1280
+ resolveHeartbeatConfig,
1281
+ isWithinActiveHours,
1282
+ pushSystemEvent,
1283
+ drainSystemEvents,
1284
+ pendingEventCount,
1285
+ HEARTBEAT_WORKER_NAME,
1286
+ heartbeatWorker,
1287
+ startHeartbeat,
1288
+ wakeHeartbeatNow,
1289
+ resolveCronDeliveryPlan,
1290
+ isOttoPayload,
1291
+ executeOttoJob,
1292
+ assertSupportedJobSpec,
1293
+ assertDeliverySupport,
1294
+ normalizeRequiredName,
1295
+ normalizeOptionalText,
1296
+ normalizeOptionalAgentId,
1297
+ applyJobPatch,
1298
+ parseAbsoluteTimeMs,
1299
+ migrateLegacyCronPayload,
1300
+ normalizeCronJobInput,
1301
+ normalizeCronJobCreate,
1302
+ normalizeCronJobPatch,
1303
+ resolveCronRunLogPath,
1304
+ appendCronRunLog,
1305
+ readCronRunLogEntries,
1306
+ resolveCronStorePath,
1307
+ loadCronStore,
1308
+ saveCronStore,
1309
+ validateScheduleTimestamp,
1310
+ otto_exports
1311
+ };