@elizaos/plugin-scheduling 2.0.3-beta.6 → 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.
Files changed (57) hide show
  1. package/dist/anchors/anchor-registry.d.ts +33 -0
  2. package/dist/anchors/anchor-registry.d.ts.map +1 -0
  3. package/dist/anchors/anchor-registry.js +129 -0
  4. package/dist/anchors/anchor-registry.js.map +1 -0
  5. package/dist/dispatch-types.d.ts +28 -0
  6. package/dist/dispatch-types.d.ts.map +1 -0
  7. package/dist/dispatch-types.js +1 -0
  8. package/dist/dispatch-types.js.map +1 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +18 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/plugin.d.ts +17 -0
  14. package/dist/plugin.d.ts.map +1 -0
  15. package/dist/plugin.js +8 -0
  16. package/dist/plugin.js.map +1 -0
  17. package/dist/scheduled-task/completion-check-registry.d.ts +19 -0
  18. package/dist/scheduled-task/completion-check-registry.d.ts.map +1 -0
  19. package/dist/scheduled-task/completion-check-registry.js +113 -0
  20. package/dist/scheduled-task/completion-check-registry.js.map +1 -0
  21. package/dist/scheduled-task/consolidation-policy.d.ts +51 -0
  22. package/dist/scheduled-task/consolidation-policy.d.ts.map +1 -0
  23. package/dist/scheduled-task/consolidation-policy.js +154 -0
  24. package/dist/scheduled-task/consolidation-policy.js.map +1 -0
  25. package/dist/scheduled-task/due.d.ts +19 -0
  26. package/dist/scheduled-task/due.d.ts.map +1 -0
  27. package/dist/scheduled-task/due.js +349 -0
  28. package/dist/scheduled-task/due.js.map +1 -0
  29. package/dist/scheduled-task/escalation.d.ts +55 -0
  30. package/dist/scheduled-task/escalation.d.ts.map +1 -0
  31. package/dist/scheduled-task/escalation.js +99 -0
  32. package/dist/scheduled-task/escalation.js.map +1 -0
  33. package/dist/scheduled-task/gate-registry.d.ts +18 -0
  34. package/dist/scheduled-task/gate-registry.d.ts.map +1 -0
  35. package/dist/scheduled-task/gate-registry.js +244 -0
  36. package/dist/scheduled-task/gate-registry.js.map +1 -0
  37. package/dist/scheduled-task/index.d.ts +20 -0
  38. package/dist/scheduled-task/index.d.ts.map +1 -0
  39. package/dist/scheduled-task/index.js +83 -0
  40. package/dist/scheduled-task/index.js.map +1 -0
  41. package/dist/scheduled-task/next-fire-at.d.ts +40 -0
  42. package/dist/scheduled-task/next-fire-at.d.ts.map +1 -0
  43. package/dist/scheduled-task/next-fire-at.js +202 -0
  44. package/dist/scheduled-task/next-fire-at.js.map +1 -0
  45. package/dist/scheduled-task/runner.d.ts +263 -0
  46. package/dist/scheduled-task/runner.d.ts.map +1 -0
  47. package/dist/scheduled-task/runner.js +721 -0
  48. package/dist/scheduled-task/runner.js.map +1 -0
  49. package/dist/scheduled-task/state-log.d.ts +56 -0
  50. package/dist/scheduled-task/state-log.d.ts.map +1 -0
  51. package/dist/scheduled-task/state-log.js +87 -0
  52. package/dist/scheduled-task/state-log.js.map +1 -0
  53. package/dist/scheduled-task/types.d.ts +368 -0
  54. package/dist/scheduled-task/types.d.ts.map +1 -0
  55. package/dist/scheduled-task/types.js +14 -0
  56. package/dist/scheduled-task/types.js.map +1 -0
  57. package/package.json +5 -5
@@ -0,0 +1,154 @@
1
+ function createAnchorRegistry() {
2
+ const map = /* @__PURE__ */ new Map();
3
+ return {
4
+ register(a, opts) {
5
+ if (!a.anchorKey || typeof a.anchorKey !== "string") {
6
+ throw new Error("AnchorRegistry.register: anchorKey required");
7
+ }
8
+ if (map.has(a.anchorKey) && !opts?.override) {
9
+ throw new Error(
10
+ `AnchorRegistry.register: duplicate anchorKey "${a.anchorKey}"`
11
+ );
12
+ }
13
+ map.set(a.anchorKey, a);
14
+ },
15
+ get(key) {
16
+ return map.get(key) ?? null;
17
+ },
18
+ list() {
19
+ return Array.from(map.values());
20
+ },
21
+ async resolve(key, context) {
22
+ const anchor = map.get(key);
23
+ if (!anchor) return null;
24
+ const result = await anchor.resolve(context);
25
+ return result ?? null;
26
+ }
27
+ };
28
+ }
29
+ function todayIsoWithLocalHHMM(nowIso, hhmm, tz) {
30
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(hhmm);
31
+ if (!match) return null;
32
+ const hour = Number.parseInt(match[1] ?? "0", 10);
33
+ const minute = Number.parseInt(match[2] ?? "0", 10);
34
+ try {
35
+ const formatter = new Intl.DateTimeFormat("en-CA", {
36
+ timeZone: tz,
37
+ year: "numeric",
38
+ month: "2-digit",
39
+ day: "2-digit"
40
+ });
41
+ const parts = formatter.formatToParts(new Date(nowIso));
42
+ const y = Number.parseInt(
43
+ parts.find((p) => p.type === "year")?.value ?? "1970",
44
+ 10
45
+ );
46
+ const mo = Number.parseInt(
47
+ parts.find((p) => p.type === "month")?.value ?? "01",
48
+ 10
49
+ );
50
+ const d = Number.parseInt(
51
+ parts.find((p) => p.type === "day")?.value ?? "01",
52
+ 10
53
+ );
54
+ const localDate = new Date(Date.UTC(y, mo - 1, d, hour, minute, 0));
55
+ const offsetFormatter = new Intl.DateTimeFormat("en-US", {
56
+ timeZone: tz,
57
+ timeZoneName: "longOffset",
58
+ year: "numeric",
59
+ month: "2-digit",
60
+ day: "2-digit",
61
+ hour: "2-digit",
62
+ minute: "2-digit",
63
+ hour12: false
64
+ });
65
+ const tzParts = offsetFormatter.formatToParts(localDate);
66
+ const offsetStr = tzParts.find((p) => p.type === "timeZoneName")?.value ?? "GMT";
67
+ const offsetMatch = /GMT([+-]\d{1,2})(?::?(\d{2}))?/.exec(offsetStr);
68
+ let offsetMinutes = 0;
69
+ if (offsetMatch) {
70
+ const sign = offsetMatch[1]?.startsWith("-") ? -1 : 1;
71
+ const oh = Math.abs(Number.parseInt(offsetMatch[1] ?? "0", 10));
72
+ const om = Number.parseInt(offsetMatch[2] ?? "0", 10);
73
+ offsetMinutes = sign * (oh * 60 + om);
74
+ }
75
+ const atMs = localDate.getTime() - offsetMinutes * 6e4;
76
+ return { atIso: new Date(atMs).toISOString() };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ const fallbackWakeConfirmedAnchor = {
82
+ anchorKey: "wake.confirmed",
83
+ describe: {
84
+ label: "Wake confirmed (ownerFact.morningWindow.start fallback)",
85
+ provider: "@elizaos/plugin-personal-assistant:scheduled-task:fallback"
86
+ },
87
+ resolve(context) {
88
+ const tz = context.ownerFacts.timezone ?? "UTC";
89
+ const start = context.ownerFacts.morningWindow?.start;
90
+ if (!start) return null;
91
+ return todayIsoWithLocalHHMM(context.nowIso, start, tz);
92
+ }
93
+ };
94
+ function registerFallbackAnchors(reg) {
95
+ if (!reg.get("wake.confirmed")) {
96
+ reg.register(fallbackWakeConfirmedAnchor);
97
+ }
98
+ }
99
+ const PRIORITY_RANK = {
100
+ high: 3,
101
+ medium: 2,
102
+ low: 1
103
+ };
104
+ function createConsolidationRegistry() {
105
+ const map = /* @__PURE__ */ new Map();
106
+ return {
107
+ register(p) {
108
+ if (!p.anchorKey || typeof p.anchorKey !== "string") {
109
+ throw new Error("ConsolidationRegistry.register: anchorKey required");
110
+ }
111
+ map.set(p.anchorKey, p);
112
+ },
113
+ get(key) {
114
+ return map.get(key) ?? null;
115
+ },
116
+ list() {
117
+ return Array.from(map.values());
118
+ },
119
+ consolidate(anchorKey, tasks) {
120
+ const policy = map.get(anchorKey) ?? null;
121
+ if (!policy || tasks.length === 0) {
122
+ return { policy, batches: tasks.map((t) => [t]) };
123
+ }
124
+ const sorted = [...tasks].sort((a, b) => {
125
+ if (policy.sortBy === "priority_desc") {
126
+ return PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority];
127
+ }
128
+ const aFired = a.state.firedAt ?? "";
129
+ const bFired = b.state.firedAt ?? "";
130
+ return aFired < bFired ? -1 : aFired > bFired ? 1 : 0;
131
+ });
132
+ const cap = policy.maxBatchSize && policy.maxBatchSize > 0 ? policy.maxBatchSize : sorted.length;
133
+ if (policy.mode === "merge") {
134
+ const batches = [];
135
+ for (let i = 0; i < sorted.length; i += cap) {
136
+ batches.push(sorted.slice(i, i + cap));
137
+ }
138
+ return { policy, batches };
139
+ }
140
+ return {
141
+ policy,
142
+ batches: sorted.map((t) => [t])
143
+ };
144
+ }
145
+ };
146
+ }
147
+ const __anchorTestUtils = { todayIsoWithLocalHHMM };
148
+ export {
149
+ __anchorTestUtils,
150
+ createAnchorRegistry,
151
+ createConsolidationRegistry,
152
+ registerFallbackAnchors
153
+ };
154
+ //# sourceMappingURL=consolidation-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scheduled-task/consolidation-policy.ts"],"sourcesContent":["/**\n * AnchorContribution + AnchorConsolidationPolicy registry.\n *\n * Ships a fallback `wake.confirmed` anchor that resolves to\n * `ownerFact.morningWindow.start`. `plugin-health` registers the richer\n * `wake.observed` / `wake.confirmed` / `bedtime.target` / `nap.start`\n * anchors and may overwrite the fallback at boot — so the fallback is only\n * registered when the real one is absent.\n *\n * Consolidation policies are referenced by anchor key. The runner uses\n * them when multiple `relative_to_anchor` tasks fire on the same anchor;\n * `mode = \"merge\"` means the runner asks consumers to render one\n * combined card; `sequential` staggers; `parallel` fires all at once.\n */\n\nimport type {\n AnchorConsolidationPolicy,\n AnchorContext,\n AnchorContribution,\n ScheduledTask,\n ScheduledTaskPriority,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Anchor registry\n// ---------------------------------------------------------------------------\n\nexport interface AnchorRegistry {\n register(a: AnchorContribution, opts?: { override?: boolean }): void;\n get(anchorKey: string): AnchorContribution | null;\n list(): AnchorContribution[];\n resolve(\n anchorKey: string,\n context: AnchorContext,\n ): Promise<{ atIso: string } | null>;\n}\n\nexport function createAnchorRegistry(): AnchorRegistry {\n const map = new Map<string, AnchorContribution>();\n return {\n register(a, opts) {\n if (!a.anchorKey || typeof a.anchorKey !== \"string\") {\n throw new Error(\"AnchorRegistry.register: anchorKey required\");\n }\n if (map.has(a.anchorKey) && !opts?.override) {\n throw new Error(\n `AnchorRegistry.register: duplicate anchorKey \"${a.anchorKey}\"`,\n );\n }\n map.set(a.anchorKey, a);\n },\n get(key) {\n return map.get(key) ?? null;\n },\n list() {\n return Array.from(map.values());\n },\n async resolve(key, context) {\n const anchor = map.get(key);\n if (!anchor) return null;\n const result = await anchor.resolve(context);\n return result ?? null;\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Fallback anchor resolver — Wave 1\n// ---------------------------------------------------------------------------\n\nfunction todayIsoWithLocalHHMM(\n nowIso: string,\n hhmm: string,\n tz: string,\n): { atIso: string } | null {\n const match = /^([01]\\d|2[0-3]):([0-5]\\d)$/.exec(hhmm);\n if (!match) return null;\n const hour = Number.parseInt(match[1] ?? \"0\", 10);\n const minute = Number.parseInt(match[2] ?? \"0\", 10);\n // Start from the local-date string for the given tz, then construct an\n // ISO that represents that local hour/minute.\n try {\n const formatter = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: tz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n const parts = formatter.formatToParts(new Date(nowIso));\n const y = Number.parseInt(\n parts.find((p) => p.type === \"year\")?.value ?? \"1970\",\n 10,\n );\n const mo = Number.parseInt(\n parts.find((p) => p.type === \"month\")?.value ?? \"01\",\n 10,\n );\n const d = Number.parseInt(\n parts.find((p) => p.type === \"day\")?.value ?? \"01\",\n 10,\n );\n // Build a UTC iso then offset by the tz offset from this date.\n // Simplest correct approach: ask Intl for the offset minutes for this\n // local datetime by formatting an offset.\n const localDate = new Date(Date.UTC(y, mo - 1, d, hour, minute, 0));\n // Compute the offset by formatting localDate in the tz and reading\n // longOffset; fall back to UTC if not supported.\n const offsetFormatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: tz,\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 tzParts = offsetFormatter.formatToParts(localDate);\n const offsetStr =\n tzParts.find((p) => p.type === \"timeZoneName\")?.value ?? \"GMT\";\n const offsetMatch = /GMT([+-]\\d{1,2})(?::?(\\d{2}))?/.exec(offsetStr);\n let offsetMinutes = 0;\n if (offsetMatch) {\n const sign = offsetMatch[1]?.startsWith(\"-\") ? -1 : 1;\n const oh = Math.abs(Number.parseInt(offsetMatch[1] ?? \"0\", 10));\n const om = Number.parseInt(offsetMatch[2] ?? \"0\", 10);\n offsetMinutes = sign * (oh * 60 + om);\n }\n const atMs = localDate.getTime() - offsetMinutes * 60_000;\n return { atIso: new Date(atMs).toISOString() };\n } catch {\n return null;\n }\n}\n\nconst fallbackWakeConfirmedAnchor: AnchorContribution = {\n anchorKey: \"wake.confirmed\",\n describe: {\n label: \"Wake confirmed (ownerFact.morningWindow.start fallback)\",\n provider: \"@elizaos/plugin-personal-assistant:scheduled-task:fallback\",\n },\n resolve(context) {\n const tz = context.ownerFacts.timezone ?? \"UTC\";\n const start = context.ownerFacts.morningWindow?.start;\n if (!start) return null;\n return todayIsoWithLocalHHMM(context.nowIso, start, tz);\n },\n};\n\nexport function registerFallbackAnchors(reg: AnchorRegistry): void {\n if (!reg.get(\"wake.confirmed\")) {\n reg.register(fallbackWakeConfirmedAnchor);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Consolidation registry\n// ---------------------------------------------------------------------------\n\nexport interface ConsolidationRegistry {\n register(p: AnchorConsolidationPolicy): void;\n get(anchorKey: string): AnchorConsolidationPolicy | null;\n list(): AnchorConsolidationPolicy[];\n /**\n * Apply a policy to a fresh batch of tasks fired on the same anchor.\n * Returns batches the runner should hand to its dispatcher; each batch\n * is a list of tasks the consumer renders together (or sequentially —\n * the policy mode tells the consumer how).\n */\n consolidate(\n anchorKey: string,\n tasks: ScheduledTask[],\n ): { policy: AnchorConsolidationPolicy | null; batches: ScheduledTask[][] };\n}\n\nconst PRIORITY_RANK: Record<ScheduledTaskPriority, number> = {\n high: 3,\n medium: 2,\n low: 1,\n};\n\nexport function createConsolidationRegistry(): ConsolidationRegistry {\n const map = new Map<string, AnchorConsolidationPolicy>();\n return {\n register(p) {\n if (!p.anchorKey || typeof p.anchorKey !== \"string\") {\n throw new Error(\"ConsolidationRegistry.register: anchorKey required\");\n }\n map.set(p.anchorKey, p);\n },\n get(key) {\n return map.get(key) ?? null;\n },\n list() {\n return Array.from(map.values());\n },\n consolidate(anchorKey, tasks) {\n const policy = map.get(anchorKey) ?? null;\n if (!policy || tasks.length === 0) {\n return { policy, batches: tasks.map((t) => [t]) };\n }\n\n const sorted = [...tasks].sort((a, b) => {\n if (policy.sortBy === \"priority_desc\") {\n return PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority];\n }\n const aFired = a.state.firedAt ?? \"\";\n const bFired = b.state.firedAt ?? \"\";\n return aFired < bFired ? -1 : aFired > bFired ? 1 : 0;\n });\n\n const cap =\n policy.maxBatchSize && policy.maxBatchSize > 0\n ? policy.maxBatchSize\n : sorted.length;\n\n if (policy.mode === \"merge\") {\n const batches: ScheduledTask[][] = [];\n for (let i = 0; i < sorted.length; i += cap) {\n batches.push(sorted.slice(i, i + cap));\n }\n return { policy, batches };\n }\n\n // sequential & parallel both produce one task per batch — the\n // difference (stagger) is observable to the dispatcher via the\n // policy reference, not via batch shape.\n return {\n policy,\n batches: sorted.map((t) => [t]),\n };\n },\n };\n}\n\nexport const __anchorTestUtils = { todayIsoWithLocalHHMM };\n"],"mappings":"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","names":[]}
@@ -0,0 +1,19 @@
1
+ import type { AnchorRegistry } from "../anchors/anchor-registry.js";
2
+ import type { OwnerFactsView, ScheduledTask, ScheduledTaskTrigger } from "./types.js";
3
+ export interface ScheduledTaskDueContext {
4
+ now: Date;
5
+ ownerFacts?: OwnerFactsView;
6
+ anchors?: AnchorRegistry | null;
7
+ }
8
+ export interface ScheduledTaskDueDecision {
9
+ due: boolean;
10
+ reason: string;
11
+ occurrenceAtIso?: string;
12
+ }
13
+ export declare function isRecurringTrigger(trigger: ScheduledTaskTrigger): boolean;
14
+ export declare function isScheduledTaskDue(task: ScheduledTask, context: ScheduledTaskDueContext): Promise<ScheduledTaskDueDecision>;
15
+ export declare function markWindowFireIfNeeded(task: ScheduledTask, context: ScheduledTaskDueContext): Record<string, unknown> | null;
16
+ export declare function isCompletionTimeoutDue(task: ScheduledTask, now: Date): ScheduledTaskDueDecision;
17
+ export declare function expectedReplyKindForTask(task: ScheduledTask): "any" | "yes_no" | "approval" | "free_form";
18
+ export declare function pendingPromptRoomIdForTask(task: ScheduledTask): string | null;
19
+ //# sourceMappingURL=due.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"due.d.ts","sourceRoot":"","sources":["../../src/scheduled-task/due.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEb,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,IAAI,CAAC;IACV,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAkBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAOzE;AA6UD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAoCnC;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAchC;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,IAAI,GACR,wBAAwB,CAwB1B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,aAAa,GAClB,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAQ7C;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAU7E"}
@@ -0,0 +1,349 @@
1
+ import { computeNextCronRunAtMs } from "@elizaos/core";
2
+ const MINUTE_MS = 6e4;
3
+ const _DAY_MS = 24 * 60 * MINUTE_MS;
4
+ const CRON_CATCHUP_WINDOW_MS = 36 * 60 * MINUTE_MS;
5
+ function parseIsoMs(value) {
6
+ if (typeof value !== "string" || value.length === 0) return null;
7
+ const parsed = Date.parse(value);
8
+ return Number.isFinite(parsed) ? parsed : null;
9
+ }
10
+ function isTerminalStatus(status) {
11
+ return status === "completed" || status === "skipped" || status === "expired" || status === "failed" || status === "dismissed";
12
+ }
13
+ function isRecurringTrigger(trigger) {
14
+ return trigger.kind === "cron" || trigger.kind === "interval" || trigger.kind === "relative_to_anchor" || trigger.kind === "during_window";
15
+ }
16
+ function localParts(date, timeZone) {
17
+ const formatter = new Intl.DateTimeFormat("en-US", {
18
+ timeZone,
19
+ year: "numeric",
20
+ month: "2-digit",
21
+ day: "2-digit",
22
+ hour: "2-digit",
23
+ minute: "2-digit",
24
+ hour12: false
25
+ });
26
+ const parts = formatter.formatToParts(date);
27
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
28
+ return {
29
+ year: read("year"),
30
+ month: read("month"),
31
+ day: read("day"),
32
+ hour: read("hour") % 24,
33
+ minute: read("minute")
34
+ };
35
+ }
36
+ function localDateKey(date, timeZone) {
37
+ const parts = localParts(date, timeZone);
38
+ return `${parts.year.toString().padStart(4, "0")}-${parts.month.toString().padStart(2, "0")}-${parts.day.toString().padStart(2, "0")}`;
39
+ }
40
+ function minutesFromHHMM(value) {
41
+ if (!value) return null;
42
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
43
+ if (!match) return null;
44
+ return Number(match[1]) * 60 + Number(match[2]);
45
+ }
46
+ function localHHMMToIso(now, hhmm, timeZone) {
47
+ const minutes = minutesFromHHMM(hhmm);
48
+ if (minutes === null) return null;
49
+ const parts = localParts(now, timeZone);
50
+ const hour = Math.floor(minutes / 60);
51
+ const minute = minutes % 60;
52
+ const localAsUtc = Date.UTC(
53
+ parts.year,
54
+ parts.month - 1,
55
+ parts.day,
56
+ hour,
57
+ minute
58
+ );
59
+ const offsetFormatter = new Intl.DateTimeFormat("en-US", {
60
+ timeZone,
61
+ timeZoneName: "longOffset",
62
+ year: "numeric",
63
+ month: "2-digit",
64
+ day: "2-digit",
65
+ hour: "2-digit",
66
+ minute: "2-digit",
67
+ hour12: false
68
+ });
69
+ const offsetParts = offsetFormatter.formatToParts(new Date(localAsUtc));
70
+ const offsetValue = offsetParts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
71
+ const offsetMatch = /GMT([+-]\d{1,2})(?::?(\d{2}))?/.exec(offsetValue);
72
+ let offsetMinutes = 0;
73
+ if (offsetMatch) {
74
+ const sign = offsetMatch[1]?.startsWith("-") ? -1 : 1;
75
+ const hours = Math.abs(Number.parseInt(offsetMatch[1] ?? "0", 10));
76
+ const minutesPart = Number.parseInt(offsetMatch[2] ?? "0", 10);
77
+ offsetMinutes = sign * (hours * 60 + minutesPart);
78
+ }
79
+ return new Date(localAsUtc - offsetMinutes * MINUTE_MS).toISOString();
80
+ }
81
+ function metadataCreatedAtMs(task) {
82
+ return parseIsoMs(task.metadata?.createdAtIso) ?? parseIsoMs(task.metadata?.createdAt) ?? parseIsoMs(task.metadata?.scheduledAtIso);
83
+ }
84
+ function wasFiredOnOrAfter(task, occurrenceMs) {
85
+ const firedAtMs = parseIsoMs(task.state.firedAt);
86
+ return firedAtMs !== null && firedAtMs >= occurrenceMs;
87
+ }
88
+ function scheduledOverrideDue(task, nowMs) {
89
+ if (task.state.status !== "scheduled") return null;
90
+ const fireAtMs = parseIsoMs(task.state.firedAt);
91
+ if (fireAtMs === null) return null;
92
+ if (fireAtMs > nowMs) {
93
+ return { due: false, reason: "scheduled_override_pending" };
94
+ }
95
+ return {
96
+ due: true,
97
+ reason: "scheduled_override_due",
98
+ occurrenceAtIso: new Date(fireAtMs).toISOString()
99
+ };
100
+ }
101
+ function onceDue(task, trigger, nowMs) {
102
+ if (task.state.firedAt) {
103
+ return { due: false, reason: "once_already_fired" };
104
+ }
105
+ const atMs = parseIsoMs(trigger.atIso);
106
+ if (atMs === null) return { due: false, reason: "once_invalid_at" };
107
+ return atMs <= nowMs ? {
108
+ due: true,
109
+ reason: "once_due",
110
+ occurrenceAtIso: new Date(atMs).toISOString()
111
+ } : { due: false, reason: "once_pending" };
112
+ }
113
+ function intervalDue(task, trigger, nowMs) {
114
+ if (!Number.isFinite(trigger.everyMinutes) || trigger.everyMinutes <= 0) {
115
+ return { due: false, reason: "interval_invalid" };
116
+ }
117
+ const fromMs = parseIsoMs(trigger.from);
118
+ const untilMs = parseIsoMs(trigger.until);
119
+ if (fromMs !== null && nowMs < fromMs) {
120
+ return { due: false, reason: "interval_before_from" };
121
+ }
122
+ if (untilMs !== null && nowMs > untilMs) {
123
+ return { due: false, reason: "interval_after_until" };
124
+ }
125
+ const lastFireMs = parseIsoMs(task.state.firedAt);
126
+ if (lastFireMs === null) {
127
+ const firstMs = fromMs ?? nowMs;
128
+ return firstMs <= nowMs ? {
129
+ due: true,
130
+ reason: "interval_first_due",
131
+ occurrenceAtIso: new Date(firstMs).toISOString()
132
+ } : { due: false, reason: "interval_first_pending" };
133
+ }
134
+ const nextMs = lastFireMs + trigger.everyMinutes * MINUTE_MS;
135
+ return nextMs <= nowMs ? {
136
+ due: true,
137
+ reason: "interval_due",
138
+ occurrenceAtIso: new Date(nextMs).toISOString()
139
+ } : { due: false, reason: "interval_pending" };
140
+ }
141
+ function cronDue(task, trigger, nowMs) {
142
+ const baseMs = parseIsoMs(task.state.firedAt) ?? metadataCreatedAtMs(task) ?? nowMs - CRON_CATCHUP_WINDOW_MS;
143
+ const nextMs = computeNextCronRunAtMs(trigger.expression, baseMs, trigger.tz);
144
+ if (nextMs === null) return { due: false, reason: "cron_invalid" };
145
+ return nextMs <= nowMs ? {
146
+ due: true,
147
+ reason: "cron_due",
148
+ occurrenceAtIso: new Date(nextMs).toISOString()
149
+ } : { due: false, reason: "cron_pending" };
150
+ }
151
+ async function resolveAnchorIso(trigger, context) {
152
+ const ownerFacts = context.ownerFacts ?? {};
153
+ const registryAnchor = context.anchors?.get(trigger.anchorKey);
154
+ if (typeof registryAnchor?.resolve === "function") {
155
+ const resolved = await registryAnchor.resolve({
156
+ nowIso: context.now.toISOString(),
157
+ ownerFacts
158
+ });
159
+ if (resolved?.atIso && Number.isFinite(Date.parse(resolved.atIso))) {
160
+ return resolved.atIso;
161
+ }
162
+ }
163
+ const timeZone = ownerFacts.timezone ?? "UTC";
164
+ if (trigger.anchorKey === "wake.confirmed" || trigger.anchorKey === "wake.observed" || trigger.anchorKey === "morning.start") {
165
+ return localHHMMToIso(
166
+ context.now,
167
+ ownerFacts.morningWindow?.start,
168
+ timeZone
169
+ );
170
+ }
171
+ if (trigger.anchorKey === "bedtime.target") {
172
+ return localHHMMToIso(context.now, ownerFacts.eveningWindow?.end, timeZone) ?? localHHMMToIso(context.now, "22:30", timeZone);
173
+ }
174
+ if (trigger.anchorKey === "night.start") {
175
+ return localHHMMToIso(
176
+ context.now,
177
+ ownerFacts.eveningWindow?.start,
178
+ timeZone
179
+ );
180
+ }
181
+ if (trigger.anchorKey === "lunch.start") {
182
+ return localHHMMToIso(context.now, "12:00", timeZone);
183
+ }
184
+ return null;
185
+ }
186
+ async function relativeAnchorDue(task, trigger, context, nowMs) {
187
+ const anchorIso = await resolveAnchorIso(trigger, context);
188
+ const anchorMs = parseIsoMs(anchorIso);
189
+ if (anchorMs === null) {
190
+ return { due: false, reason: "anchor_unresolved" };
191
+ }
192
+ const occurrenceMs = anchorMs + trigger.offsetMinutes * MINUTE_MS;
193
+ if (occurrenceMs > nowMs) {
194
+ return { due: false, reason: "anchor_pending" };
195
+ }
196
+ if (wasFiredOnOrAfter(task, occurrenceMs)) {
197
+ return { due: false, reason: "anchor_already_fired" };
198
+ }
199
+ return {
200
+ due: true,
201
+ reason: "anchor_due",
202
+ occurrenceAtIso: new Date(occurrenceMs).toISOString()
203
+ };
204
+ }
205
+ function windowBoundsMinutes(windowKey, ownerFacts) {
206
+ const morningStart = minutesFromHHMM(ownerFacts.morningWindow?.start) ?? 6 * 60;
207
+ const morningEnd = minutesFromHHMM(ownerFacts.morningWindow?.end) ?? 11 * 60;
208
+ const eveningStart = minutesFromHHMM(ownerFacts.eveningWindow?.start) ?? 18 * 60;
209
+ const eveningEnd = minutesFromHHMM(ownerFacts.eveningWindow?.end) ?? 22 * 60;
210
+ const afternoonStart = morningEnd;
211
+ const afternoonEnd = eveningStart;
212
+ const windows = {
213
+ morning: [{ name: "morning", start: morningStart, end: morningEnd }],
214
+ afternoon: [
215
+ { name: "afternoon", start: afternoonStart, end: afternoonEnd }
216
+ ],
217
+ evening: [{ name: "evening", start: eveningStart, end: eveningEnd }],
218
+ night: [
219
+ { name: "night", start: eveningEnd, end: 24 * 60 },
220
+ { name: "night", start: 0, end: morningStart }
221
+ ],
222
+ morning_or_night: [
223
+ { name: "morning", start: morningStart, end: morningEnd },
224
+ { name: "night", start: eveningEnd, end: 24 * 60 },
225
+ { name: "night", start: 0, end: morningStart }
226
+ ],
227
+ morning_or_evening: [
228
+ { name: "morning", start: morningStart, end: morningEnd },
229
+ { name: "evening", start: eveningStart, end: eveningEnd }
230
+ ]
231
+ };
232
+ return windows[windowKey] ?? [];
233
+ }
234
+ function duringWindowDue(task, trigger, context) {
235
+ const ownerFacts = context.ownerFacts ?? {};
236
+ const timeZone = ownerFacts.timezone ?? "UTC";
237
+ const parts = localParts(context.now, timeZone);
238
+ const nowMinutes = parts.hour * 60 + parts.minute;
239
+ const windows = windowBoundsMinutes(trigger.windowKey, ownerFacts);
240
+ const active = windows.find(
241
+ (window) => nowMinutes >= window.start && nowMinutes < window.end
242
+ );
243
+ if (!active) return { due: false, reason: "window_inactive" };
244
+ const fireKey = `${localDateKey(context.now, timeZone)}:${trigger.windowKey}:${active.name}`;
245
+ if (task.metadata?.lastWindowFireKey === fireKey) {
246
+ return { due: false, reason: "window_already_fired" };
247
+ }
248
+ return {
249
+ due: true,
250
+ reason: "window_due",
251
+ occurrenceAtIso: context.now.toISOString()
252
+ };
253
+ }
254
+ async function isScheduledTaskDue(task, context) {
255
+ if (task.state.status === "dismissed") {
256
+ return { due: false, reason: "dismissed" };
257
+ }
258
+ if (isTerminalStatus(task.state.status) && !isRecurringTrigger(task.trigger)) {
259
+ return { due: false, reason: "terminal_non_recurring" };
260
+ }
261
+ const nowMs = context.now.getTime();
262
+ const override = scheduledOverrideDue(task, nowMs);
263
+ if (override) return override;
264
+ switch (task.trigger.kind) {
265
+ case "once":
266
+ return onceDue(task, task.trigger, nowMs);
267
+ case "interval":
268
+ return intervalDue(task, task.trigger, nowMs);
269
+ case "cron":
270
+ return cronDue(task, task.trigger, nowMs);
271
+ case "relative_to_anchor":
272
+ return relativeAnchorDue(task, task.trigger, context, nowMs);
273
+ case "during_window":
274
+ return duringWindowDue(task, task.trigger, context);
275
+ case "manual":
276
+ return { due: false, reason: "manual" };
277
+ case "event":
278
+ return { due: false, reason: "event_driven" };
279
+ case "after_task":
280
+ return { due: false, reason: "after_task_pipeline_driven" };
281
+ default: {
282
+ const exhaustive = task.trigger;
283
+ return { due: false, reason: `unknown:${String(exhaustive)}` };
284
+ }
285
+ }
286
+ }
287
+ function markWindowFireIfNeeded(task, context) {
288
+ if (task.trigger.kind !== "during_window") return null;
289
+ const ownerFacts = context.ownerFacts ?? {};
290
+ const timeZone = ownerFacts.timezone ?? "UTC";
291
+ const parts = localParts(context.now, timeZone);
292
+ const nowMinutes = parts.hour * 60 + parts.minute;
293
+ const active = windowBoundsMinutes(task.trigger.windowKey, ownerFacts).find(
294
+ (window) => nowMinutes >= window.start && nowMinutes < window.end
295
+ );
296
+ if (!active) return null;
297
+ return {
298
+ ...task.metadata ?? {},
299
+ lastWindowFireKey: `${localDateKey(context.now, timeZone)}:${task.trigger.windowKey}:${active.name}`
300
+ };
301
+ }
302
+ function isCompletionTimeoutDue(task, now) {
303
+ if (task.state.status !== "fired") {
304
+ return { due: false, reason: "not_fired" };
305
+ }
306
+ const followupAfterMinutes = task.completionCheck?.followupAfterMinutes;
307
+ if (typeof followupAfterMinutes !== "number" || !Number.isFinite(followupAfterMinutes) || followupAfterMinutes <= 0) {
308
+ return { due: false, reason: "no_completion_timeout" };
309
+ }
310
+ const firedAtMs = parseIsoMs(task.state.firedAt);
311
+ if (firedAtMs === null) {
312
+ return { due: false, reason: "missing_fired_at" };
313
+ }
314
+ const timeoutMs = firedAtMs + followupAfterMinutes * MINUTE_MS;
315
+ return timeoutMs <= now.getTime() ? {
316
+ due: true,
317
+ reason: "completion_timeout_due",
318
+ occurrenceAtIso: new Date(timeoutMs).toISOString()
319
+ } : { due: false, reason: "completion_timeout_pending" };
320
+ }
321
+ function expectedReplyKindForTask(task) {
322
+ if (task.kind === "approval" || task.completionCheck?.kind === "approval") {
323
+ return "approval";
324
+ }
325
+ if (task.completionCheck?.kind === "user_acknowledged") {
326
+ return "yes_no";
327
+ }
328
+ return "free_form";
329
+ }
330
+ function pendingPromptRoomIdForTask(task) {
331
+ const metadataRoomId = task.metadata?.pendingPromptRoomId;
332
+ if (typeof metadataRoomId === "string" && metadataRoomId.length > 0) {
333
+ return metadataRoomId;
334
+ }
335
+ const target = task.output?.target;
336
+ if (typeof target !== "string") return null;
337
+ const [channelKey, roomId] = target.split(":", 2);
338
+ if (channelKey === "in_app" && roomId) return roomId;
339
+ return null;
340
+ }
341
+ export {
342
+ expectedReplyKindForTask,
343
+ isCompletionTimeoutDue,
344
+ isRecurringTrigger,
345
+ isScheduledTaskDue,
346
+ markWindowFireIfNeeded,
347
+ pendingPromptRoomIdForTask
348
+ };
349
+ //# sourceMappingURL=due.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scheduled-task/due.ts"],"sourcesContent":["import { computeNextCronRunAtMs } from \"@elizaos/core\";\n\nimport type { AnchorRegistry } from \"../anchors/anchor-registry.js\";\nimport type {\n OwnerFactsView,\n ScheduledTask,\n ScheduledTaskStatus,\n ScheduledTaskTrigger,\n} from \"./types.js\";\n\nconst MINUTE_MS = 60_000;\nconst _DAY_MS = 24 * 60 * MINUTE_MS;\nconst CRON_CATCHUP_WINDOW_MS = 36 * 60 * MINUTE_MS;\n\nexport interface ScheduledTaskDueContext {\n now: Date;\n ownerFacts?: OwnerFactsView;\n anchors?: AnchorRegistry | null;\n}\n\nexport interface ScheduledTaskDueDecision {\n due: boolean;\n reason: string;\n occurrenceAtIso?: string;\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 isTerminalStatus(status: ScheduledTaskStatus): boolean {\n return (\n status === \"completed\" ||\n status === \"skipped\" ||\n status === \"expired\" ||\n status === \"failed\" ||\n status === \"dismissed\"\n );\n}\n\nexport function isRecurringTrigger(trigger: ScheduledTaskTrigger): boolean {\n return (\n trigger.kind === \"cron\" ||\n trigger.kind === \"interval\" ||\n trigger.kind === \"relative_to_anchor\" ||\n trigger.kind === \"during_window\"\n );\n}\n\nfunction localParts(\n date: Date,\n timeZone: string,\n): {\n year: number;\n month: number;\n day: number;\n hour: number;\n minute: number;\n} {\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 localDateKey(date: Date, timeZone: string): string {\n const parts = localParts(date, timeZone);\n return `${parts.year.toString().padStart(4, \"0\")}-${parts.month\n .toString()\n .padStart(2, \"0\")}-${parts.day.toString().padStart(2, \"0\")}`;\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 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 metadataCreatedAtMs(task: ScheduledTask): number | null {\n return (\n parseIsoMs(task.metadata?.createdAtIso) ??\n parseIsoMs(task.metadata?.createdAt) ??\n parseIsoMs(task.metadata?.scheduledAtIso)\n );\n}\n\nfunction wasFiredOnOrAfter(task: ScheduledTask, occurrenceMs: number): boolean {\n const firedAtMs = parseIsoMs(task.state.firedAt);\n return firedAtMs !== null && firedAtMs >= occurrenceMs;\n}\n\nfunction scheduledOverrideDue(\n task: ScheduledTask,\n nowMs: number,\n): ScheduledTaskDueDecision | null {\n if (task.state.status !== \"scheduled\") return null;\n const fireAtMs = parseIsoMs(task.state.firedAt);\n if (fireAtMs === null) return null;\n if (fireAtMs > nowMs) {\n return { due: false, reason: \"scheduled_override_pending\" };\n }\n return {\n due: true,\n reason: \"scheduled_override_due\",\n occurrenceAtIso: new Date(fireAtMs).toISOString(),\n };\n}\n\nfunction onceDue(\n task: ScheduledTask,\n trigger: Extract<ScheduledTaskTrigger, { kind: \"once\" }>,\n nowMs: number,\n): ScheduledTaskDueDecision {\n if (task.state.firedAt) {\n return { due: false, reason: \"once_already_fired\" };\n }\n const atMs = parseIsoMs(trigger.atIso);\n if (atMs === null) return { due: false, reason: \"once_invalid_at\" };\n return atMs <= nowMs\n ? {\n due: true,\n reason: \"once_due\",\n occurrenceAtIso: new Date(atMs).toISOString(),\n }\n : { due: false, reason: \"once_pending\" };\n}\n\nfunction intervalDue(\n task: ScheduledTask,\n trigger: Extract<ScheduledTaskTrigger, { kind: \"interval\" }>,\n nowMs: number,\n): ScheduledTaskDueDecision {\n if (!Number.isFinite(trigger.everyMinutes) || trigger.everyMinutes <= 0) {\n return { due: false, reason: \"interval_invalid\" };\n }\n const fromMs = parseIsoMs(trigger.from);\n const untilMs = parseIsoMs(trigger.until);\n if (fromMs !== null && nowMs < fromMs) {\n return { due: false, reason: \"interval_before_from\" };\n }\n if (untilMs !== null && nowMs > untilMs) {\n return { due: false, reason: \"interval_after_until\" };\n }\n const lastFireMs = parseIsoMs(task.state.firedAt);\n if (lastFireMs === null) {\n const firstMs = fromMs ?? nowMs;\n return firstMs <= nowMs\n ? {\n due: true,\n reason: \"interval_first_due\",\n occurrenceAtIso: new Date(firstMs).toISOString(),\n }\n : { due: false, reason: \"interval_first_pending\" };\n }\n const nextMs = lastFireMs + trigger.everyMinutes * MINUTE_MS;\n return nextMs <= nowMs\n ? {\n due: true,\n reason: \"interval_due\",\n occurrenceAtIso: new Date(nextMs).toISOString(),\n }\n : { due: false, reason: \"interval_pending\" };\n}\n\nfunction cronDue(\n task: ScheduledTask,\n trigger: Extract<ScheduledTaskTrigger, { kind: \"cron\" }>,\n nowMs: number,\n): ScheduledTaskDueDecision {\n const baseMs =\n parseIsoMs(task.state.firedAt) ??\n metadataCreatedAtMs(task) ??\n nowMs - CRON_CATCHUP_WINDOW_MS;\n const nextMs = computeNextCronRunAtMs(trigger.expression, baseMs, trigger.tz);\n if (nextMs === null) return { due: false, reason: \"cron_invalid\" };\n return nextMs <= nowMs\n ? {\n due: true,\n reason: \"cron_due\",\n occurrenceAtIso: new Date(nextMs).toISOString(),\n }\n : { due: false, reason: \"cron_pending\" };\n}\n\nasync function resolveAnchorIso(\n trigger: Extract<ScheduledTaskTrigger, { kind: \"relative_to_anchor\" }>,\n context: ScheduledTaskDueContext,\n): Promise<string | null> {\n const ownerFacts = context.ownerFacts ?? {};\n const registryAnchor = context.anchors?.get(trigger.anchorKey) as {\n resolve?: (\n ctx: unknown,\n ) => Promise<{ atIso: string } | null> | { atIso: string } | null;\n } | null;\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 resolved.atIso;\n }\n }\n\n const timeZone = ownerFacts.timezone ?? \"UTC\";\n if (\n trigger.anchorKey === \"wake.confirmed\" ||\n trigger.anchorKey === \"wake.observed\" ||\n trigger.anchorKey === \"morning.start\"\n ) {\n return localHHMMToIso(\n context.now,\n ownerFacts.morningWindow?.start,\n timeZone,\n );\n }\n if (trigger.anchorKey === \"bedtime.target\") {\n return (\n localHHMMToIso(context.now, ownerFacts.eveningWindow?.end, timeZone) ??\n localHHMMToIso(context.now, \"22:30\", timeZone)\n );\n }\n if (trigger.anchorKey === \"night.start\") {\n return localHHMMToIso(\n context.now,\n ownerFacts.eveningWindow?.start,\n timeZone,\n );\n }\n if (trigger.anchorKey === \"lunch.start\") {\n return localHHMMToIso(context.now, \"12:00\", timeZone);\n }\n return null;\n}\n\nasync function relativeAnchorDue(\n task: ScheduledTask,\n trigger: Extract<ScheduledTaskTrigger, { kind: \"relative_to_anchor\" }>,\n context: ScheduledTaskDueContext,\n nowMs: number,\n): Promise<ScheduledTaskDueDecision> {\n const anchorIso = await resolveAnchorIso(trigger, context);\n const anchorMs = parseIsoMs(anchorIso);\n if (anchorMs === null) {\n return { due: false, reason: \"anchor_unresolved\" };\n }\n const occurrenceMs = anchorMs + trigger.offsetMinutes * MINUTE_MS;\n if (occurrenceMs > nowMs) {\n return { due: false, reason: \"anchor_pending\" };\n }\n if (wasFiredOnOrAfter(task, occurrenceMs)) {\n return { due: false, reason: \"anchor_already_fired\" };\n }\n return {\n due: true,\n reason: \"anchor_due\",\n occurrenceAtIso: new Date(occurrenceMs).toISOString(),\n };\n}\n\nfunction windowBoundsMinutes(\n windowKey: string,\n ownerFacts: OwnerFactsView,\n): Array<{ name: string; start: number; end: number }> {\n const morningStart =\n minutesFromHHMM(ownerFacts.morningWindow?.start) ?? 6 * 60;\n const morningEnd = minutesFromHHMM(ownerFacts.morningWindow?.end) ?? 11 * 60;\n const eveningStart =\n minutesFromHHMM(ownerFacts.eveningWindow?.start) ?? 18 * 60;\n const eveningEnd = minutesFromHHMM(ownerFacts.eveningWindow?.end) ?? 22 * 60;\n const afternoonStart = morningEnd;\n const afternoonEnd = eveningStart;\n const windows: Record<\n string,\n Array<{ name: string; start: number; end: number }>\n > = {\n morning: [{ name: \"morning\", start: morningStart, end: morningEnd }],\n afternoon: [\n { name: \"afternoon\", start: afternoonStart, end: afternoonEnd },\n ],\n evening: [{ name: \"evening\", start: eveningStart, end: eveningEnd }],\n night: [\n { name: \"night\", start: eveningEnd, end: 24 * 60 },\n { name: \"night\", start: 0, end: morningStart },\n ],\n morning_or_night: [\n { name: \"morning\", start: morningStart, end: morningEnd },\n { name: \"night\", start: eveningEnd, end: 24 * 60 },\n { name: \"night\", start: 0, end: morningStart },\n ],\n morning_or_evening: [\n { name: \"morning\", start: morningStart, end: morningEnd },\n { name: \"evening\", start: eveningStart, end: eveningEnd },\n ],\n };\n return windows[windowKey] ?? [];\n}\n\nfunction duringWindowDue(\n task: ScheduledTask,\n trigger: Extract<ScheduledTaskTrigger, { kind: \"during_window\" }>,\n context: ScheduledTaskDueContext,\n): ScheduledTaskDueDecision {\n const ownerFacts = context.ownerFacts ?? {};\n const timeZone = ownerFacts.timezone ?? \"UTC\";\n const parts = localParts(context.now, timeZone);\n const nowMinutes = parts.hour * 60 + parts.minute;\n const windows = windowBoundsMinutes(trigger.windowKey, ownerFacts);\n const active = windows.find(\n (window) => nowMinutes >= window.start && nowMinutes < window.end,\n );\n if (!active) return { due: false, reason: \"window_inactive\" };\n const fireKey = `${localDateKey(context.now, timeZone)}:${trigger.windowKey}:${active.name}`;\n if (task.metadata?.lastWindowFireKey === fireKey) {\n return { due: false, reason: \"window_already_fired\" };\n }\n return {\n due: true,\n reason: \"window_due\",\n occurrenceAtIso: context.now.toISOString(),\n };\n}\n\nexport async function isScheduledTaskDue(\n task: ScheduledTask,\n context: ScheduledTaskDueContext,\n): Promise<ScheduledTaskDueDecision> {\n if (task.state.status === \"dismissed\") {\n return { due: false, reason: \"dismissed\" };\n }\n if (\n isTerminalStatus(task.state.status) &&\n !isRecurringTrigger(task.trigger)\n ) {\n return { due: false, reason: \"terminal_non_recurring\" };\n }\n const nowMs = context.now.getTime();\n const override = scheduledOverrideDue(task, nowMs);\n if (override) return override;\n\n switch (task.trigger.kind) {\n case \"once\":\n return onceDue(task, task.trigger, nowMs);\n case \"interval\":\n return intervalDue(task, task.trigger, nowMs);\n case \"cron\":\n return cronDue(task, task.trigger, nowMs);\n case \"relative_to_anchor\":\n return relativeAnchorDue(task, task.trigger, context, nowMs);\n case \"during_window\":\n return duringWindowDue(task, task.trigger, context);\n case \"manual\":\n return { due: false, reason: \"manual\" };\n case \"event\":\n return { due: false, reason: \"event_driven\" };\n case \"after_task\":\n return { due: false, reason: \"after_task_pipeline_driven\" };\n default: {\n const exhaustive: never = task.trigger;\n return { due: false, reason: `unknown:${String(exhaustive)}` };\n }\n }\n}\n\nexport function markWindowFireIfNeeded(\n task: ScheduledTask,\n context: ScheduledTaskDueContext,\n): Record<string, unknown> | null {\n if (task.trigger.kind !== \"during_window\") return null;\n const ownerFacts = context.ownerFacts ?? {};\n const timeZone = ownerFacts.timezone ?? \"UTC\";\n const parts = localParts(context.now, timeZone);\n const nowMinutes = parts.hour * 60 + parts.minute;\n const active = windowBoundsMinutes(task.trigger.windowKey, ownerFacts).find(\n (window) => nowMinutes >= window.start && nowMinutes < window.end,\n );\n if (!active) return null;\n return {\n ...(task.metadata ?? {}),\n lastWindowFireKey: `${localDateKey(context.now, timeZone)}:${task.trigger.windowKey}:${active.name}`,\n };\n}\n\nexport function isCompletionTimeoutDue(\n task: ScheduledTask,\n now: Date,\n): ScheduledTaskDueDecision {\n if (task.state.status !== \"fired\") {\n return { due: false, reason: \"not_fired\" };\n }\n const followupAfterMinutes = task.completionCheck?.followupAfterMinutes;\n if (\n typeof followupAfterMinutes !== \"number\" ||\n !Number.isFinite(followupAfterMinutes) ||\n followupAfterMinutes <= 0\n ) {\n return { due: false, reason: \"no_completion_timeout\" };\n }\n const firedAtMs = parseIsoMs(task.state.firedAt);\n if (firedAtMs === null) {\n return { due: false, reason: \"missing_fired_at\" };\n }\n const timeoutMs = firedAtMs + followupAfterMinutes * MINUTE_MS;\n return timeoutMs <= now.getTime()\n ? {\n due: true,\n reason: \"completion_timeout_due\",\n occurrenceAtIso: new Date(timeoutMs).toISOString(),\n }\n : { due: false, reason: \"completion_timeout_pending\" };\n}\n\nexport function expectedReplyKindForTask(\n task: ScheduledTask,\n): \"any\" | \"yes_no\" | \"approval\" | \"free_form\" {\n if (task.kind === \"approval\" || task.completionCheck?.kind === \"approval\") {\n return \"approval\";\n }\n if (task.completionCheck?.kind === \"user_acknowledged\") {\n return \"yes_no\";\n }\n return \"free_form\";\n}\n\nexport function pendingPromptRoomIdForTask(task: ScheduledTask): string | null {\n const metadataRoomId = task.metadata?.pendingPromptRoomId;\n if (typeof metadataRoomId === \"string\" && metadataRoomId.length > 0) {\n return metadataRoomId;\n }\n const target = task.output?.target;\n if (typeof target !== \"string\") return null;\n const [channelKey, roomId] = target.split(\":\", 2);\n if (channelKey === \"in_app\" && roomId) return roomId;\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Escalation evaluator.
3
+ *
4
+ * Snooze policy: snooze RESETS the ladder to step 0 at the new fire time.
5
+ * Default ladders by priority (when `escalation` is undefined):
6
+ * - low → no ladder (single attempt)
7
+ * - medium → 1 retry @ 30 min
8
+ * - high → 3-step cross-channel
9
+ *
10
+ * Callers may register additional named ladders that override
11
+ * `priority_<level>_default` keys.
12
+ */
13
+ import type { EscalationStep, ScheduledTask, ScheduledTaskPriority } from "./types.js";
14
+ export interface EscalationLadder {
15
+ ladderKey: string;
16
+ steps: EscalationStep[];
17
+ }
18
+ export interface EscalationLadderRegistry {
19
+ register(ladder: EscalationLadder, opts?: {
20
+ override?: boolean;
21
+ }): void;
22
+ get(ladderKey: string): EscalationLadder | null;
23
+ list(): EscalationLadder[];
24
+ }
25
+ export declare function createEscalationLadderRegistry(): EscalationLadderRegistry;
26
+ export declare const PRIORITY_DEFAULT_LADDER_KEYS: Record<ScheduledTaskPriority, string>;
27
+ export declare const DEFAULT_ESCALATION_LADDERS: Readonly<Record<string, EscalationLadder>>;
28
+ export declare function registerDefaultEscalationLadders(reg: EscalationLadderRegistry): void;
29
+ /**
30
+ * Resolve the effective ladder for a task. Inline `escalation.steps` win
31
+ * over `escalation.ladderKey` resolution. If neither is set, the
32
+ * priority-default ladder is returned.
33
+ */
34
+ export declare function resolveEffectiveLadder(task: ScheduledTask, registry: EscalationLadderRegistry): EscalationLadder;
35
+ export interface EscalationCursor {
36
+ /** Current step index. -1 = escalation has not started. */
37
+ stepIndex: number;
38
+ /** ISO of the most recent dispatch (or task fire for step -1). */
39
+ lastDispatchedAt: string;
40
+ }
41
+ /**
42
+ * Compute the next escalation step (returns `null` when ladder exhausted).
43
+ * `lastDispatchedAt` is the anchor for the next delay calculation.
44
+ */
45
+ export declare function nextEscalationStep(ladder: EscalationLadder, cursor: EscalationCursor): {
46
+ step: EscalationStep;
47
+ nextStepIndex: number;
48
+ fireAtIso: string;
49
+ } | null;
50
+ /**
51
+ * Snooze resets the ladder to step 0 at the new fire time.
52
+ * Returns the cursor the runner should persist post-snooze.
53
+ */
54
+ export declare function resetLadderForSnooze(newFireAtIso: string): EscalationCursor;
55
+ //# sourceMappingURL=escalation.d.ts.map