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