@elizaos/plugin-scheduling 2.0.3-beta.5 → 2.0.3-beta.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/anchors/anchor-registry.d.ts +33 -0
- package/dist/anchors/anchor-registry.d.ts.map +1 -0
- package/dist/anchors/anchor-registry.js +129 -0
- package/dist/anchors/anchor-registry.js.map +1 -0
- package/dist/dispatch-types.d.ts +28 -0
- package/dist/dispatch-types.d.ts.map +1 -0
- package/dist/dispatch-types.js +1 -0
- package/dist/dispatch-types.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +8 -0
- package/dist/plugin.js.map +1 -0
- package/dist/scheduled-task/completion-check-registry.d.ts +19 -0
- package/dist/scheduled-task/completion-check-registry.d.ts.map +1 -0
- package/dist/scheduled-task/completion-check-registry.js +113 -0
- package/dist/scheduled-task/completion-check-registry.js.map +1 -0
- package/dist/scheduled-task/consolidation-policy.d.ts +51 -0
- package/dist/scheduled-task/consolidation-policy.d.ts.map +1 -0
- package/dist/scheduled-task/consolidation-policy.js +154 -0
- package/dist/scheduled-task/consolidation-policy.js.map +1 -0
- package/dist/scheduled-task/due.d.ts +19 -0
- package/dist/scheduled-task/due.d.ts.map +1 -0
- package/dist/scheduled-task/due.js +349 -0
- package/dist/scheduled-task/due.js.map +1 -0
- package/dist/scheduled-task/escalation.d.ts +55 -0
- package/dist/scheduled-task/escalation.d.ts.map +1 -0
- package/dist/scheduled-task/escalation.js +99 -0
- package/dist/scheduled-task/escalation.js.map +1 -0
- package/dist/scheduled-task/gate-registry.d.ts +18 -0
- package/dist/scheduled-task/gate-registry.d.ts.map +1 -0
- package/dist/scheduled-task/gate-registry.js +244 -0
- package/dist/scheduled-task/gate-registry.js.map +1 -0
- package/dist/scheduled-task/index.d.ts +20 -0
- package/dist/scheduled-task/index.d.ts.map +1 -0
- package/dist/scheduled-task/index.js +83 -0
- package/dist/scheduled-task/index.js.map +1 -0
- package/dist/scheduled-task/next-fire-at.d.ts +40 -0
- package/dist/scheduled-task/next-fire-at.d.ts.map +1 -0
- package/dist/scheduled-task/next-fire-at.js +202 -0
- package/dist/scheduled-task/next-fire-at.js.map +1 -0
- package/dist/scheduled-task/runner.d.ts +263 -0
- package/dist/scheduled-task/runner.d.ts.map +1 -0
- package/dist/scheduled-task/runner.js +721 -0
- package/dist/scheduled-task/runner.js.map +1 -0
- package/dist/scheduled-task/state-log.d.ts +56 -0
- package/dist/scheduled-task/state-log.d.ts.map +1 -0
- package/dist/scheduled-task/state-log.js +87 -0
- package/dist/scheduled-task/state-log.js.map +1 -0
- package/dist/scheduled-task/types.d.ts +368 -0
- package/dist/scheduled-task/types.d.ts.map +1 -0
- package/dist/scheduled-task/types.js +14 -0
- package/dist/scheduled-task/types.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { computeNextCronRunAtMs } from "@elizaos/core";
|
|
2
|
+
const MINUTE_MS = 6e4;
|
|
3
|
+
function parseIsoMs(value) {
|
|
4
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
5
|
+
const parsed = Date.parse(value);
|
|
6
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
7
|
+
}
|
|
8
|
+
function minutesFromHHMM(value) {
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
13
|
+
}
|
|
14
|
+
function localParts(date, timeZone) {
|
|
15
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
16
|
+
timeZone,
|
|
17
|
+
year: "numeric",
|
|
18
|
+
month: "2-digit",
|
|
19
|
+
day: "2-digit",
|
|
20
|
+
hour: "2-digit",
|
|
21
|
+
minute: "2-digit",
|
|
22
|
+
hour12: false
|
|
23
|
+
});
|
|
24
|
+
const parts = formatter.formatToParts(date);
|
|
25
|
+
const read = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
|
26
|
+
return {
|
|
27
|
+
year: read("year"),
|
|
28
|
+
month: read("month"),
|
|
29
|
+
day: read("day"),
|
|
30
|
+
hour: read("hour") % 24,
|
|
31
|
+
minute: read("minute")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function localHHMMToIso(now, hhmm, timeZone) {
|
|
35
|
+
const minutes = minutesFromHHMM(hhmm);
|
|
36
|
+
if (minutes === null) return null;
|
|
37
|
+
const parts = localParts(now, timeZone);
|
|
38
|
+
const hour = Math.floor(minutes / 60);
|
|
39
|
+
const minute = minutes % 60;
|
|
40
|
+
const localAsUtc = Date.UTC(
|
|
41
|
+
parts.year,
|
|
42
|
+
parts.month - 1,
|
|
43
|
+
parts.day,
|
|
44
|
+
hour,
|
|
45
|
+
minute
|
|
46
|
+
);
|
|
47
|
+
const offsetFormatter = new Intl.DateTimeFormat("en-US", {
|
|
48
|
+
timeZone,
|
|
49
|
+
timeZoneName: "longOffset",
|
|
50
|
+
year: "numeric",
|
|
51
|
+
month: "2-digit",
|
|
52
|
+
day: "2-digit",
|
|
53
|
+
hour: "2-digit",
|
|
54
|
+
minute: "2-digit",
|
|
55
|
+
hour12: false
|
|
56
|
+
});
|
|
57
|
+
const offsetParts = offsetFormatter.formatToParts(new Date(localAsUtc));
|
|
58
|
+
const offsetValue = offsetParts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
|
|
59
|
+
const offsetMatch = /GMT([+-]\d{1,2})(?::?(\d{2}))?/.exec(offsetValue);
|
|
60
|
+
let offsetMinutes = 0;
|
|
61
|
+
if (offsetMatch) {
|
|
62
|
+
const sign = offsetMatch[1]?.startsWith("-") ? -1 : 1;
|
|
63
|
+
const hours = Math.abs(Number.parseInt(offsetMatch[1] ?? "0", 10));
|
|
64
|
+
const minutesPart = Number.parseInt(offsetMatch[2] ?? "0", 10);
|
|
65
|
+
offsetMinutes = sign * (hours * 60 + minutesPart);
|
|
66
|
+
}
|
|
67
|
+
return new Date(localAsUtc - offsetMinutes * MINUTE_MS).toISOString();
|
|
68
|
+
}
|
|
69
|
+
function nextWindowStartIso(windowKey, context) {
|
|
70
|
+
const facts = context.ownerFacts;
|
|
71
|
+
const timeZone = facts.timezone ?? "UTC";
|
|
72
|
+
const morningStart = facts.morningWindow?.start;
|
|
73
|
+
const eveningStart = facts.eveningWindow?.start;
|
|
74
|
+
const eveningEnd = facts.eveningWindow?.end;
|
|
75
|
+
let candidates = [];
|
|
76
|
+
switch (windowKey) {
|
|
77
|
+
case "morning":
|
|
78
|
+
candidates = [localHHMMToIso(context.now, morningStart, timeZone) ?? ""];
|
|
79
|
+
break;
|
|
80
|
+
case "afternoon":
|
|
81
|
+
candidates = [
|
|
82
|
+
localHHMMToIso(context.now, facts.morningWindow?.end, timeZone) ?? ""
|
|
83
|
+
];
|
|
84
|
+
break;
|
|
85
|
+
case "evening":
|
|
86
|
+
candidates = [localHHMMToIso(context.now, eveningStart, timeZone) ?? ""];
|
|
87
|
+
break;
|
|
88
|
+
case "night":
|
|
89
|
+
candidates = [
|
|
90
|
+
localHHMMToIso(context.now, eveningEnd, timeZone) ?? "",
|
|
91
|
+
localHHMMToIso(context.now, "00:00", timeZone) ?? ""
|
|
92
|
+
];
|
|
93
|
+
break;
|
|
94
|
+
case "morning_or_night":
|
|
95
|
+
candidates = [
|
|
96
|
+
localHHMMToIso(context.now, morningStart, timeZone) ?? "",
|
|
97
|
+
localHHMMToIso(context.now, eveningEnd, timeZone) ?? ""
|
|
98
|
+
];
|
|
99
|
+
break;
|
|
100
|
+
case "morning_or_evening":
|
|
101
|
+
candidates = [
|
|
102
|
+
localHHMMToIso(context.now, morningStart, timeZone) ?? "",
|
|
103
|
+
localHHMMToIso(context.now, eveningStart, timeZone) ?? ""
|
|
104
|
+
];
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const nowMs = context.now.getTime();
|
|
110
|
+
const upcoming = candidates.map((iso) => parseIsoMs(iso)).filter((ms) => ms !== null);
|
|
111
|
+
if (upcoming.length === 0) return null;
|
|
112
|
+
const future = upcoming.find((ms) => ms >= nowMs);
|
|
113
|
+
if (future !== void 0) return new Date(future).toISOString();
|
|
114
|
+
const earliest = Math.min(...upcoming);
|
|
115
|
+
return new Date(earliest + 24 * 60 * MINUTE_MS).toISOString();
|
|
116
|
+
}
|
|
117
|
+
async function nextAnchorIso(trigger, context) {
|
|
118
|
+
const ownerFacts = context.ownerFacts;
|
|
119
|
+
const registryAnchor = context.anchors?.get(trigger.anchorKey);
|
|
120
|
+
if (typeof registryAnchor?.resolve === "function") {
|
|
121
|
+
const resolved = await registryAnchor.resolve({
|
|
122
|
+
nowIso: context.now.toISOString(),
|
|
123
|
+
ownerFacts
|
|
124
|
+
});
|
|
125
|
+
if (resolved?.atIso && Number.isFinite(Date.parse(resolved.atIso))) {
|
|
126
|
+
return new Date(
|
|
127
|
+
Date.parse(resolved.atIso) + trigger.offsetMinutes * MINUTE_MS
|
|
128
|
+
).toISOString();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const timeZone = ownerFacts.timezone ?? "UTC";
|
|
132
|
+
let baseIso = null;
|
|
133
|
+
if (trigger.anchorKey === "wake.confirmed" || trigger.anchorKey === "wake.observed" || trigger.anchorKey === "morning.start") {
|
|
134
|
+
baseIso = localHHMMToIso(
|
|
135
|
+
context.now,
|
|
136
|
+
ownerFacts.morningWindow?.start,
|
|
137
|
+
timeZone
|
|
138
|
+
);
|
|
139
|
+
} else if (trigger.anchorKey === "bedtime.target") {
|
|
140
|
+
baseIso = localHHMMToIso(context.now, ownerFacts.eveningWindow?.end, timeZone) ?? localHHMMToIso(context.now, "22:30", timeZone);
|
|
141
|
+
} else if (trigger.anchorKey === "night.start") {
|
|
142
|
+
baseIso = localHHMMToIso(
|
|
143
|
+
context.now,
|
|
144
|
+
ownerFacts.eveningWindow?.start,
|
|
145
|
+
timeZone
|
|
146
|
+
);
|
|
147
|
+
} else if (trigger.anchorKey === "lunch.start") {
|
|
148
|
+
baseIso = localHHMMToIso(context.now, "12:00", timeZone);
|
|
149
|
+
}
|
|
150
|
+
if (!baseIso) return null;
|
|
151
|
+
return new Date(
|
|
152
|
+
Date.parse(baseIso) + trigger.offsetMinutes * MINUTE_MS
|
|
153
|
+
).toISOString();
|
|
154
|
+
}
|
|
155
|
+
async function computeNextFireAt(task, context) {
|
|
156
|
+
const trigger = task.trigger;
|
|
157
|
+
switch (trigger.kind) {
|
|
158
|
+
case "once": {
|
|
159
|
+
if (task.state.firedAt) return null;
|
|
160
|
+
const at = Date.parse(trigger.atIso);
|
|
161
|
+
if (!Number.isFinite(at)) return null;
|
|
162
|
+
return new Date(at).toISOString();
|
|
163
|
+
}
|
|
164
|
+
case "cron": {
|
|
165
|
+
const lastFire = parseIsoMs(task.state.firedAt);
|
|
166
|
+
const baseMs = lastFire !== null && lastFire >= context.now.getTime() ? lastFire : context.now.getTime();
|
|
167
|
+
const nextMs = computeNextCronRunAtMs(
|
|
168
|
+
trigger.expression,
|
|
169
|
+
baseMs,
|
|
170
|
+
trigger.tz
|
|
171
|
+
);
|
|
172
|
+
return nextMs === null ? null : new Date(nextMs).toISOString();
|
|
173
|
+
}
|
|
174
|
+
case "interval": {
|
|
175
|
+
if (!Number.isFinite(trigger.everyMinutes) || trigger.everyMinutes <= 0) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const fromMs = parseIsoMs(trigger.from);
|
|
179
|
+
const untilMs = parseIsoMs(trigger.until);
|
|
180
|
+
const lastFireMs = parseIsoMs(task.state.firedAt);
|
|
181
|
+
const candidateMs = lastFireMs !== null ? lastFireMs + trigger.everyMinutes * MINUTE_MS : fromMs ?? context.now.getTime();
|
|
182
|
+
if (untilMs !== null && candidateMs > untilMs) return null;
|
|
183
|
+
return new Date(candidateMs).toISOString();
|
|
184
|
+
}
|
|
185
|
+
case "relative_to_anchor":
|
|
186
|
+
return nextAnchorIso(trigger, context);
|
|
187
|
+
case "during_window":
|
|
188
|
+
return nextWindowStartIso(trigger.windowKey, context);
|
|
189
|
+
case "event":
|
|
190
|
+
case "manual":
|
|
191
|
+
case "after_task":
|
|
192
|
+
return null;
|
|
193
|
+
default: {
|
|
194
|
+
const _exhaustive = trigger;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
computeNextFireAt
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=next-fire-at.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/scheduled-task/next-fire-at.ts"],"sourcesContent":["/**\n * Computes the indexed `next_fire_at` timestamp for a `ScheduledTask`.\n *\n * The scheduler tick (`processDueScheduledTasks`) filters by this column to\n * avoid scanning every row in `life_scheduled_tasks` once per minute. The\n * value is approximate — it is a \"next candidate fire time\" that the\n * authoritative `isScheduledTaskDue` re-evaluates per task. Triggers that\n * wake on external signals (`event`, `manual`, `after_task`) leave it NULL.\n *\n * Computed for: `once`, `cron`, `interval`, `relative_to_anchor`,\n * `during_window`.\n *\n * Computed by the runner on every state mutation that can change the\n * upcoming fire time: `schedule()`, `apply(\"snooze\")`, `apply(\"edit\")`, and\n * the post-fire/post-skip persistence in `fire()`.\n */\n\nimport { computeNextCronRunAtMs } from \"@elizaos/core\";\n\nimport type { AnchorRegistry } from \"../anchors/anchor-registry.js\";\nimport type {\n OwnerFactsView,\n ScheduledTask,\n ScheduledTaskTrigger,\n} from \"./types.js\";\n\nconst MINUTE_MS = 60_000;\n\nexport interface ComputeNextFireAtContext {\n now: Date;\n ownerFacts: OwnerFactsView;\n anchors?: AnchorRegistry | null;\n}\n\nfunction parseIsoMs(value: unknown): number | null {\n if (typeof value !== \"string\" || value.length === 0) return null;\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nfunction minutesFromHHMM(value: string | undefined): number | null {\n if (!value) return null;\n const match = /^([01]\\d|2[0-3]):([0-5]\\d)$/.exec(value);\n if (!match) return null;\n return Number(match[1]) * 60 + Number(match[2]);\n}\n\nfunction localParts(\n date: Date,\n timeZone: string,\n): { year: number; month: number; day: number; hour: number; minute: number } {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n const parts = formatter.formatToParts(date);\n const read = (type: string): number =>\n Number(parts.find((part) => part.type === type)?.value ?? 0);\n return {\n year: read(\"year\"),\n month: read(\"month\"),\n day: read(\"day\"),\n hour: read(\"hour\") % 24,\n minute: read(\"minute\"),\n };\n}\n\nfunction localHHMMToIso(\n now: Date,\n hhmm: string | undefined,\n timeZone: string,\n): string | null {\n const minutes = minutesFromHHMM(hhmm);\n if (minutes === null) return null;\n const parts = localParts(now, timeZone);\n const hour = Math.floor(minutes / 60);\n const minute = minutes % 60;\n const localAsUtc = Date.UTC(\n parts.year,\n parts.month - 1,\n parts.day,\n hour,\n minute,\n );\n const offsetFormatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n timeZoneName: \"longOffset\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n const offsetParts = offsetFormatter.formatToParts(new Date(localAsUtc));\n const offsetValue =\n offsetParts.find((part) => part.type === \"timeZoneName\")?.value ?? \"GMT\";\n const offsetMatch = /GMT([+-]\\d{1,2})(?::?(\\d{2}))?/.exec(offsetValue);\n let offsetMinutes = 0;\n if (offsetMatch) {\n const sign = offsetMatch[1]?.startsWith(\"-\") ? -1 : 1;\n const hours = Math.abs(Number.parseInt(offsetMatch[1] ?? \"0\", 10));\n const minutesPart = Number.parseInt(offsetMatch[2] ?? \"0\", 10);\n offsetMinutes = sign * (hours * 60 + minutesPart);\n }\n return new Date(localAsUtc - offsetMinutes * MINUTE_MS).toISOString();\n}\n\nfunction nextWindowStartIso(\n windowKey: string,\n context: ComputeNextFireAtContext,\n): string | null {\n const facts = context.ownerFacts;\n const timeZone = facts.timezone ?? \"UTC\";\n const morningStart = facts.morningWindow?.start;\n const eveningStart = facts.eveningWindow?.start;\n const eveningEnd = facts.eveningWindow?.end;\n let candidates: string[] = [];\n switch (windowKey) {\n case \"morning\":\n candidates = [localHHMMToIso(context.now, morningStart, timeZone) ?? \"\"];\n break;\n case \"afternoon\":\n candidates = [\n localHHMMToIso(context.now, facts.morningWindow?.end, timeZone) ?? \"\",\n ];\n break;\n case \"evening\":\n candidates = [localHHMMToIso(context.now, eveningStart, timeZone) ?? \"\"];\n break;\n case \"night\":\n candidates = [\n localHHMMToIso(context.now, eveningEnd, timeZone) ?? \"\",\n localHHMMToIso(context.now, \"00:00\", timeZone) ?? \"\",\n ];\n break;\n case \"morning_or_night\":\n candidates = [\n localHHMMToIso(context.now, morningStart, timeZone) ?? \"\",\n localHHMMToIso(context.now, eveningEnd, timeZone) ?? \"\",\n ];\n break;\n case \"morning_or_evening\":\n candidates = [\n localHHMMToIso(context.now, morningStart, timeZone) ?? \"\",\n localHHMMToIso(context.now, eveningStart, timeZone) ?? \"\",\n ];\n break;\n default:\n return null;\n }\n const nowMs = context.now.getTime();\n const upcoming = candidates\n .map((iso) => parseIsoMs(iso))\n .filter((ms): ms is number => ms !== null);\n if (upcoming.length === 0) return null;\n const future = upcoming.find((ms) => ms >= nowMs);\n if (future !== undefined) return new Date(future).toISOString();\n // All today's window-starts are in the past; bump to same local-HHMM\n // tomorrow. The runner re-computes after each fire, so we only need a\n // coarse \"tomorrow morning\" candidate here.\n const earliest = Math.min(...upcoming);\n return new Date(earliest + 24 * 60 * MINUTE_MS).toISOString();\n}\n\nasync function nextAnchorIso(\n trigger: Extract<ScheduledTaskTrigger, { kind: \"relative_to_anchor\" }>,\n context: ComputeNextFireAtContext,\n): Promise<string | null> {\n const ownerFacts = context.ownerFacts;\n const registryAnchor = context.anchors?.get(trigger.anchorKey) as\n | {\n resolve?: (\n ctx: unknown,\n ) => Promise<{ atIso: string } | null> | { atIso: string } | null;\n }\n | null\n | undefined;\n if (typeof registryAnchor?.resolve === \"function\") {\n const resolved = await registryAnchor.resolve({\n nowIso: context.now.toISOString(),\n ownerFacts,\n });\n if (resolved?.atIso && Number.isFinite(Date.parse(resolved.atIso))) {\n return new Date(\n Date.parse(resolved.atIso) + trigger.offsetMinutes * MINUTE_MS,\n ).toISOString();\n }\n }\n\n const timeZone = ownerFacts.timezone ?? \"UTC\";\n let baseIso: string | null = null;\n if (\n trigger.anchorKey === \"wake.confirmed\" ||\n trigger.anchorKey === \"wake.observed\" ||\n trigger.anchorKey === \"morning.start\"\n ) {\n baseIso = localHHMMToIso(\n context.now,\n ownerFacts.morningWindow?.start,\n timeZone,\n );\n } else if (trigger.anchorKey === \"bedtime.target\") {\n baseIso =\n localHHMMToIso(context.now, ownerFacts.eveningWindow?.end, timeZone) ??\n localHHMMToIso(context.now, \"22:30\", timeZone);\n } else if (trigger.anchorKey === \"night.start\") {\n baseIso = localHHMMToIso(\n context.now,\n ownerFacts.eveningWindow?.start,\n timeZone,\n );\n } else if (trigger.anchorKey === \"lunch.start\") {\n baseIso = localHHMMToIso(context.now, \"12:00\", timeZone);\n }\n if (!baseIso) return null;\n return new Date(\n Date.parse(baseIso) + trigger.offsetMinutes * MINUTE_MS,\n ).toISOString();\n}\n\n/**\n * Compute the next-fire-at timestamp for a task. Returns null when the\n * trigger does not have a wall-clock fire time (event/manual/after_task)\n * or when the inputs cannot be resolved (e.g. unknown anchor key).\n *\n * The function is async because anchor resolution may consult the runtime\n * anchor registry (e.g. `wake.confirmed` reads the latest activity signal).\n *\n * Inputs:\n * - `task`: must have its current `trigger` and (post-fire) `state.firedAt`.\n * - `context.now`: clock used for forward-projecting cron/interval/window.\n *\n * Outputs an ISO string, never a Date — the caller writes directly to a\n * Postgres timestamp column.\n */\nexport async function computeNextFireAt(\n task: Pick<ScheduledTask, \"trigger\" | \"state\" | \"metadata\">,\n context: ComputeNextFireAtContext,\n): Promise<string | null> {\n const trigger = task.trigger;\n switch (trigger.kind) {\n case \"once\": {\n if (task.state.firedAt) return null;\n const at = Date.parse(trigger.atIso);\n if (!Number.isFinite(at)) return null;\n return new Date(at).toISOString();\n }\n case \"cron\": {\n const lastFire = parseIsoMs(task.state.firedAt);\n const baseMs =\n lastFire !== null && lastFire >= context.now.getTime()\n ? lastFire\n : context.now.getTime();\n const nextMs = computeNextCronRunAtMs(\n trigger.expression,\n baseMs,\n trigger.tz,\n );\n return nextMs === null ? null : new Date(nextMs).toISOString();\n }\n case \"interval\": {\n if (!Number.isFinite(trigger.everyMinutes) || trigger.everyMinutes <= 0) {\n return null;\n }\n const fromMs = parseIsoMs(trigger.from);\n const untilMs = parseIsoMs(trigger.until);\n const lastFireMs = parseIsoMs(task.state.firedAt);\n const candidateMs =\n lastFireMs !== null\n ? lastFireMs + trigger.everyMinutes * MINUTE_MS\n : (fromMs ?? context.now.getTime());\n if (untilMs !== null && candidateMs > untilMs) return null;\n return new Date(candidateMs).toISOString();\n }\n case \"relative_to_anchor\":\n return nextAnchorIso(trigger, context);\n case \"during_window\":\n return nextWindowStartIso(trigger.windowKey, context);\n case \"event\":\n case \"manual\":\n case \"after_task\":\n return null;\n default: {\n const _exhaustive: never = trigger;\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScheduledTaskRunner.
|
|
3
|
+
*
|
|
4
|
+
* Cross-agent invariants enforced here:
|
|
5
|
+
* - The runner does NOT pattern-match on `promptInstructions`.
|
|
6
|
+
* - `acknowledged` is non-terminal; `pipeline.onComplete` only fires on
|
|
7
|
+
* `completed`.
|
|
8
|
+
* - Snooze RESETS the ladder.
|
|
9
|
+
* - Global pause skips tasks with `respectsGlobalPause: true`.
|
|
10
|
+
* - `shouldFire` is always an array; empty / missing arrays are treated as
|
|
11
|
+
* "no gates → allow".
|
|
12
|
+
* - `idempotencyKey` deduplicates schedules.
|
|
13
|
+
* - `pipeline.onSkip` wins over `completionCheck.followupAfterMinutes` when
|
|
14
|
+
* both are set.
|
|
15
|
+
*/
|
|
16
|
+
import type { DispatchResult } from "../dispatch-types.js";
|
|
17
|
+
import type { CompletionCheckRegistry } from "./completion-check-registry.js";
|
|
18
|
+
import type { AnchorRegistry, ConsolidationRegistry } from "./consolidation-policy.js";
|
|
19
|
+
import { type EscalationLadderRegistry } from "./escalation.js";
|
|
20
|
+
import type { TaskGateRegistry } from "./gate-registry.js";
|
|
21
|
+
import { type ScheduledTaskLogStore } from "./state-log.js";
|
|
22
|
+
import { type ActivitySignalBusView, type GlobalPauseView, type OwnerFactsView, type ScheduledTask, type ScheduledTaskFilter, type ScheduledTaskRunner, type SubjectStoreView, type TaskExecutionProfile } from "./types.js";
|
|
23
|
+
/**
|
|
24
|
+
* Typed error thrown by `runner.schedule()` when an `escalation.steps[].channelKey`
|
|
25
|
+
* does not match a registered channel in the host runtime's `ChannelRegistry`.
|
|
26
|
+
* The runner stays decoupled from the channel registry implementation; the
|
|
27
|
+
* caller injects a `channelKeys()` lookup via {@link ScheduledTaskRunnerDeps}.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ChannelKeyError extends Error {
|
|
30
|
+
readonly channelKey: string;
|
|
31
|
+
readonly available: readonly string[];
|
|
32
|
+
readonly code = "channel_key_unknown";
|
|
33
|
+
constructor(channelKey: string, available: readonly string[]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Options the runner passes to `store.upsert` to keep the indexed
|
|
37
|
+
* `next_fire_at` column in sync with the task's current trigger and state.
|
|
38
|
+
*
|
|
39
|
+
* The store does not compute this itself — the runner computes the value
|
|
40
|
+
* using the active anchor / owner-facts / now references and forwards it
|
|
41
|
+
* here. The repository writes a Postgres `timestamp with time zone`
|
|
42
|
+
* (NULL for triggers without a wall-clock fire time).
|
|
43
|
+
*/
|
|
44
|
+
export interface ScheduledTaskUpsertOptions {
|
|
45
|
+
nextFireAtIso: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Outcome of the atomic fire-claim. Exactly one parallel call resolves to
|
|
49
|
+
* `"fired"` for a given `(taskId, status="scheduled")` row; concurrent
|
|
50
|
+
* callers see `"raced"` because the UPDATE … WHERE status='scheduled' clause
|
|
51
|
+
* matches zero rows after the first wins.
|
|
52
|
+
*
|
|
53
|
+
* `task` on the `"fired"` branch carries the post-claim state (status =
|
|
54
|
+
* "fired", `firedAt` set to the claim instant, `nextFireAt` cleared so the
|
|
55
|
+
* scheduler tick will not re-pick it up before the next mutation).
|
|
56
|
+
*/
|
|
57
|
+
export type ScheduledTaskClaimResult = {
|
|
58
|
+
kind: "fired";
|
|
59
|
+
task: ScheduledTask;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "raced";
|
|
62
|
+
};
|
|
63
|
+
export interface ScheduledTaskStore {
|
|
64
|
+
upsert(task: ScheduledTask, options?: ScheduledTaskUpsertOptions): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Atomically transition a row from `state.status === "scheduled"` to
|
|
67
|
+
* `"fired"`, returning the resulting row. Returns `{ kind: "raced" }`
|
|
68
|
+
* when zero rows matched — either because the task is already past
|
|
69
|
+
* `scheduled` (another tick claimed it) or the id no longer exists.
|
|
70
|
+
*
|
|
71
|
+
* The store is the only place where the read-mutate-write becomes
|
|
72
|
+
* atomic; the runner's previous read-then-upsert pattern was racy
|
|
73
|
+
* across parallel ticks. See `LifeOpsRepository.claimScheduledTaskForFire`.
|
|
74
|
+
*/
|
|
75
|
+
claimForFire(args: {
|
|
76
|
+
taskId: string;
|
|
77
|
+
firedAtIso: string;
|
|
78
|
+
}): Promise<ScheduledTaskClaimResult>;
|
|
79
|
+
get(taskId: string): Promise<ScheduledTask | null>;
|
|
80
|
+
findByIdempotencyKey(key: string): Promise<ScheduledTask | null>;
|
|
81
|
+
list(filter?: ScheduledTaskFilter): Promise<ScheduledTask[]>;
|
|
82
|
+
delete(taskId: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export declare function createInMemoryScheduledTaskStore(): ScheduledTaskStore;
|
|
85
|
+
export interface ScheduledTaskDispatchRecord {
|
|
86
|
+
taskId: string;
|
|
87
|
+
firedAtIso: string;
|
|
88
|
+
channelKey: string;
|
|
89
|
+
intensity?: "soft" | "normal" | "urgent";
|
|
90
|
+
promptInstructions: string;
|
|
91
|
+
contextRequest: ScheduledTask["contextRequest"];
|
|
92
|
+
consolidationBatchId?: string;
|
|
93
|
+
output?: ScheduledTask["output"];
|
|
94
|
+
}
|
|
95
|
+
export interface ScheduledTaskDispatcher {
|
|
96
|
+
dispatch(record: ScheduledTaskDispatchRecord): Promise<DispatchResult | undefined>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Test-only no-op dispatcher. Production code MUST inject
|
|
100
|
+
* `createProductionScheduledTaskDispatcher` via runtime-wiring; the runner
|
|
101
|
+
* factory requires a dispatcher and there is no silent fallback. Exported only
|
|
102
|
+
* so tests can construct a runner without touching the channel layer.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export declare const TestNoopScheduledTaskDispatcher: ScheduledTaskDispatcher;
|
|
107
|
+
export interface ScheduledTaskRunnerDeps {
|
|
108
|
+
agentId: string;
|
|
109
|
+
store: ScheduledTaskStore;
|
|
110
|
+
logStore: ScheduledTaskLogStore;
|
|
111
|
+
gates: TaskGateRegistry;
|
|
112
|
+
completionChecks: CompletionCheckRegistry;
|
|
113
|
+
ladders: EscalationLadderRegistry;
|
|
114
|
+
anchors: AnchorRegistry;
|
|
115
|
+
consolidation: ConsolidationRegistry;
|
|
116
|
+
ownerFacts: () => OwnerFactsView | Promise<OwnerFactsView>;
|
|
117
|
+
globalPause: GlobalPauseView;
|
|
118
|
+
activity: ActivitySignalBusView;
|
|
119
|
+
subjectStore: SubjectStoreView;
|
|
120
|
+
dispatcher: ScheduledTaskDispatcher;
|
|
121
|
+
/**
|
|
122
|
+
* Lookup of registered `ChannelRegistry` keys. When supplied, `schedule()`
|
|
123
|
+
* validates each `escalation.steps[].channelKey` against this set and
|
|
124
|
+
* throws {@link ChannelKeyError} on miss. Decoupled from the channels
|
|
125
|
+
* module to keep the spine free of channel-layer dependencies.
|
|
126
|
+
*/
|
|
127
|
+
channelKeys?: () => ReadonlySet<string>;
|
|
128
|
+
/**
|
|
129
|
+
* Returns the set of `TaskExecutionProfile` values the current host can
|
|
130
|
+
* actually run. The runner consults this AFTER the atomic fire-claim but
|
|
131
|
+
* BEFORE dispatch: if `task.executionProfile` is not in the set, dispatch
|
|
132
|
+
* is rewritten to `notify-only` and a `"substituted"` state-log row is
|
|
133
|
+
* recorded. Default (when not provided): all four profiles available —
|
|
134
|
+
* appropriate for tests and Node desktop. Mobile / Capacitor callers
|
|
135
|
+
* inject a real probe from
|
|
136
|
+
* `@elizaos/app-core/services/local-inference/host-capabilities`.
|
|
137
|
+
*/
|
|
138
|
+
hostCapabilities?: () => ReadonlySet<TaskExecutionProfile>;
|
|
139
|
+
/** Override for tests. */
|
|
140
|
+
newTaskId?: () => string;
|
|
141
|
+
/** Override for tests. */
|
|
142
|
+
now?: () => Date;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Public read view of `metadata.escalationCursor`.
|
|
146
|
+
*
|
|
147
|
+
* The cursor is the runner's persistence channel for the snooze-resets-ladder
|
|
148
|
+
* rule. Consumers that need to surface "currently on step N of escalation"
|
|
149
|
+
* read it through {@link ScheduledTaskRunnerExtras.getEscalationCursor} so
|
|
150
|
+
* they don't reach into the metadata namespace directly.
|
|
151
|
+
*
|
|
152
|
+
* - `stepIndex` follows the {@link EscalationCursor} convention: `-1` means
|
|
153
|
+
* the task was fired but no escalation step has been dispatched yet;
|
|
154
|
+
* `0..n` is the index into the resolved ladder's `steps`.
|
|
155
|
+
* - `lastFiredAt` is the ISO of the most recent dispatch (or the initial
|
|
156
|
+
* task fire when `stepIndex === -1`).
|
|
157
|
+
* - `channelKey` is resolved from the effective ladder. For `stepIndex === -1`
|
|
158
|
+
* we surface the first step's channel when the ladder has steps, falling
|
|
159
|
+
* back to `"in_app"` when the ladder is empty.
|
|
160
|
+
*/
|
|
161
|
+
export interface EscalationCursorView {
|
|
162
|
+
stepIndex: number;
|
|
163
|
+
lastFiredAt: string;
|
|
164
|
+
channelKey: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Strict result of a single `fire()` attempt. Callers should exhaustively
|
|
168
|
+
* switch on `kind`.
|
|
169
|
+
*
|
|
170
|
+
* - `fired` — the task transitioned to `"fired"` (or was deferred via
|
|
171
|
+
* `gate.defer`, reopened for a recurrence, etc.) and the dispatcher ran.
|
|
172
|
+
* `task` is the post-mutation state.
|
|
173
|
+
* - `raced` — another tick atomically claimed this task first. Caller drops
|
|
174
|
+
* the attempt silently; the winning tick's dispatch is authoritative.
|
|
175
|
+
* - `skipped` — the task was skipped without dispatch: global-pause active,
|
|
176
|
+
* a gate denied, or the task was already terminal and not eligible for
|
|
177
|
+
* recurrence refire.
|
|
178
|
+
* - `dispatch_failed` — the atomic claim succeeded but the dispatcher threw.
|
|
179
|
+
* The runner persists the row as `"failed"` and writes a failed state-log
|
|
180
|
+
* entry so history does not strand the task as successfully fired.
|
|
181
|
+
*/
|
|
182
|
+
export type ScheduledTaskFireResult = {
|
|
183
|
+
kind: "fired";
|
|
184
|
+
task: ScheduledTask;
|
|
185
|
+
} | {
|
|
186
|
+
kind: "raced";
|
|
187
|
+
taskId: string;
|
|
188
|
+
} | {
|
|
189
|
+
kind: "skipped";
|
|
190
|
+
task: ScheduledTask;
|
|
191
|
+
reason: string;
|
|
192
|
+
} | {
|
|
193
|
+
kind: "dispatch_failed";
|
|
194
|
+
task: ScheduledTask;
|
|
195
|
+
error: Error;
|
|
196
|
+
};
|
|
197
|
+
export interface ScheduledTaskRunnerExtras {
|
|
198
|
+
/**
|
|
199
|
+
* Convenience wrapper around {@link ScheduledTaskRunnerExtras.fireWithResult}
|
|
200
|
+
* that flattens the discriminated union into a `ScheduledTask`. Returns
|
|
201
|
+
* the post-fire task on `fired` / `skipped` / `dispatch_failed`, and the
|
|
202
|
+
* still-`scheduled` task on `raced` (so legacy callers that re-read see
|
|
203
|
+
* the unmodified row). The strict-fire callsite — `processDueScheduledTasks`
|
|
204
|
+
* — uses `fireWithResult` directly.
|
|
205
|
+
*
|
|
206
|
+
* Exposed for tests so we can assert behavior deterministically without
|
|
207
|
+
* waiting on a real timer, and for legacy actions that only want the
|
|
208
|
+
* task back.
|
|
209
|
+
*/
|
|
210
|
+
fire(taskId: string, args?: {
|
|
211
|
+
eventPayload?: unknown;
|
|
212
|
+
allowTerminalRefire?: boolean;
|
|
213
|
+
}): Promise<ScheduledTask>;
|
|
214
|
+
/**
|
|
215
|
+
* Strict fire-attempt. Returns the {@link ScheduledTaskFireResult}
|
|
216
|
+
* discriminated union; callers must exhaustively switch on `kind`. This
|
|
217
|
+
* is the path the scheduler tick uses so the `raced` outcome (another
|
|
218
|
+
* tick claimed the same row first) is observable instead of silently
|
|
219
|
+
* collapsed into a "fired" return.
|
|
220
|
+
*/
|
|
221
|
+
fireWithResult(taskId: string, args?: {
|
|
222
|
+
eventPayload?: unknown;
|
|
223
|
+
allowTerminalRefire?: boolean;
|
|
224
|
+
}): Promise<ScheduledTaskFireResult>;
|
|
225
|
+
/**
|
|
226
|
+
* Re-evaluate completion for a fired task (e.g. user_replied_within
|
|
227
|
+
* scenarios, late inbounds). The runner consults its registered
|
|
228
|
+
* completion-check and may transition the task to `completed`.
|
|
229
|
+
*/
|
|
230
|
+
evaluateCompletion(taskId: string, signal: {
|
|
231
|
+
acknowledged?: boolean;
|
|
232
|
+
repliedAtIso?: string;
|
|
233
|
+
}): Promise<ScheduledTask>;
|
|
234
|
+
/**
|
|
235
|
+
* Run the nightly rollup pass on the state-log. Default retention is 90
|
|
236
|
+
* days.
|
|
237
|
+
*/
|
|
238
|
+
rolloverStateLog(opts?: {
|
|
239
|
+
retentionDays?: number;
|
|
240
|
+
}): Promise<{
|
|
241
|
+
rolledUp: number;
|
|
242
|
+
deletedRaw: number;
|
|
243
|
+
}>;
|
|
244
|
+
/**
|
|
245
|
+
* Return all gates registered (for the dev-registries endpoint).
|
|
246
|
+
*/
|
|
247
|
+
inspectRegistries(): {
|
|
248
|
+
gates: string[];
|
|
249
|
+
completionChecks: string[];
|
|
250
|
+
ladders: string[];
|
|
251
|
+
anchors: string[];
|
|
252
|
+
consolidationPolicies: string[];
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Read the public view of `metadata.escalationCursor` for a task. Returns
|
|
256
|
+
* `null` when the task is not found or has no cursor recorded yet.
|
|
257
|
+
*/
|
|
258
|
+
getEscalationCursor(taskId: string): Promise<EscalationCursorView | null>;
|
|
259
|
+
}
|
|
260
|
+
export interface ScheduledTaskRunnerHandle extends ScheduledTaskRunner, ScheduledTaskRunnerExtras {
|
|
261
|
+
}
|
|
262
|
+
export declare function createScheduledTaskRunner(deps: ScheduledTaskRunnerDeps): ScheduledTaskRunnerHandle;
|
|
263
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/scheduled-task/runner.ts"],"names":[],"mappings":"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"}
|