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