@memberjunction/scheduling-engine 5.44.0 → 5.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @fileoverview Pure, side-effect-free logic for the User Routines dispatcher.
3
+ *
4
+ * Everything in this module is deterministic and unit-testable without a database:
5
+ * due-evaluation (activation window + NextRunAt), next-run/claim computation (cron with a
6
+ * StartAt floor), OnChange result hashing, the notify-condition matrix, and recipient
7
+ * ordering. The `UserRoutineDispatcherDriver` composes these primitives with the entity
8
+ * layer; `MJUserRoutineEntityServer` (in @memberjunction/core-entities-server) reuses
9
+ * `ComputeRoutineNextRunAt` so the entity save path and the dispatcher compute NextRunAt
10
+ * with the SAME cron helper and StartAt semantics.
11
+ *
12
+ * @module @memberjunction/scheduling-engine
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { CronExpressionHelper } from './CronExpressionHelper.js';
16
+ /**
17
+ * Same 1-second tolerance the ScheduledJobEngine applies when comparing NextRunAt to the
18
+ * evaluation time, so a routine whose NextRunAt lands a few hundred ms after the sweep
19
+ * timestamp still runs on this pass instead of waiting a full dispatcher interval.
20
+ */
21
+ export const ROUTINE_DUE_TOLERANCE_MS = 1000;
22
+ /**
23
+ * Compute the next run time for a routine: the first cron occurrence strictly after
24
+ * `fromDate`, floored by the routine's activation-window start. When `startAt` is in the
25
+ * future relative to `fromDate`, the next occurrence is computed from `startAt` instead —
26
+ * an Active routine never gets a NextRunAt before its window opens.
27
+ *
28
+ * @throws when the cron expression or timezone is invalid (callers validate first via
29
+ * {@link CronExpressionHelper.ValidateExpression} / entity Validate()).
30
+ */
31
+ export function ComputeRoutineNextRunAt(cronExpression, timezone, fromDate, startAt) {
32
+ const effectiveFrom = startAt != null && startAt.getTime() > fromDate.getTime() ? startAt : fromDate;
33
+ return CronExpressionHelper.GetNextRunTime(cronExpression, timezone || 'UTC', effectiveFrom);
34
+ }
35
+ /**
36
+ * Activation-window check (independent of NextRunAt):
37
+ * - StartAt: NULL = eligible immediately; otherwise eligible once `StartAt <= now`.
38
+ * - EndAt: NULL = no end; otherwise eligible only while `EndAt > now` (automatic sunset —
39
+ * a routine whose EndAt equals the evaluation time has already ended).
40
+ */
41
+ export function IsRoutineWithinActivationWindow(fields, now) {
42
+ if (fields.StartAt != null && fields.StartAt.getTime() > now.getTime()) {
43
+ return false;
44
+ }
45
+ if (fields.EndAt != null && fields.EndAt.getTime() <= now.getTime()) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ /**
51
+ * Full due-evaluation: Active + inside the activation window + NextRunAt set and passed
52
+ * (within {@link ROUTINE_DUE_TOLERANCE_MS}). A NULL NextRunAt is never "due" — it means the
53
+ * routine needs seeding (see {@link RoutineNeedsSeeding}).
54
+ */
55
+ export function IsRoutineDue(fields, now) {
56
+ if (fields.Status !== 'Active') {
57
+ return false;
58
+ }
59
+ if (!IsRoutineWithinActivationWindow(fields, now)) {
60
+ return false;
61
+ }
62
+ if (fields.NextRunAt == null) {
63
+ return false;
64
+ }
65
+ return fields.NextRunAt.getTime() <= now.getTime() + ROUTINE_DUE_TOLERANCE_MS;
66
+ }
67
+ /**
68
+ * A routine "needs seeding" when it is Active, inside its activation window, and has never
69
+ * had a NextRunAt computed (newly created outside the entity-server save path, or legacy
70
+ * rows). The dispatcher computes + saves NextRunAt for these WITHOUT running them — they
71
+ * become due on a later sweep once their first cron occurrence passes.
72
+ */
73
+ export function RoutineNeedsSeeding(fields, now) {
74
+ return fields.Status === 'Active' && fields.NextRunAt == null && IsRoutineWithinActivationWindow(fields, now);
75
+ }
76
+ /**
77
+ * SQL prefilter for the dispatcher's due-routine sweep. Matches Active routines inside
78
+ * their activation window whose NextRunAt is NULL (seeding candidates) or has passed.
79
+ * Every row returned is re-verified in JS via {@link IsRoutineDue} / {@link RoutineNeedsSeeding}
80
+ * — the SQL filter only narrows the sweep, it is not the source of truth.
81
+ */
82
+ export function BuildDueRoutineFilter(nowIso) {
83
+ return `Status='Active'` +
84
+ ` AND (StartAt IS NULL OR StartAt <= '${nowIso}')` +
85
+ ` AND (EndAt IS NULL OR EndAt > '${nowIso}')` +
86
+ ` AND (NextRunAt IS NULL OR NextRunAt <= '${nowIso}')`;
87
+ }
88
+ /**
89
+ * Matches ISO-8601 date-time tokens (with optional fractional seconds and timezone) —
90
+ * volatile "when did this run" markers that many targets embed in their results (e.g. the
91
+ * Calculate Expression action's `evaluatedAt`). They are execution metadata, not result
92
+ * content, so OnChange normalization strips them before hashing.
93
+ */
94
+ const ISO_TIMESTAMP_PATTERN = /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g;
95
+ /**
96
+ * SHA-256 hex digest of the normalized result content. Normalization strips embedded
97
+ * ISO-8601 timestamps (volatile execution metadata that would otherwise register a
98
+ * "change" on every run), collapses whitespace runs to single spaces, and trims — so
99
+ * cosmetic formatting differences never trigger OnChange detection.
100
+ * Null/undefined content hashes as the empty string — deterministic, never throws.
101
+ */
102
+ export function ComputeResultHash(content) {
103
+ const normalized = (content ?? '')
104
+ .replace(ISO_TIMESTAMP_PATTERN, '')
105
+ .replace(/\s+/g, ' ')
106
+ .trim();
107
+ return createHash('sha256').update(normalized, 'utf8').digest('hex');
108
+ }
109
+ /**
110
+ * The notify-condition matrix. Only terminal outcomes (Success/Failed) can notify —
111
+ * Running/Skipped never do, regardless of condition.
112
+ *
113
+ * - `Always`: any terminal outcome.
114
+ * - `OnSuccess`: Status === 'Success'.
115
+ * - `OnFailure`: Status === 'Failed'.
116
+ * - `OnChange`: the run's ResultHash differs from the routine's prior LastResultHash.
117
+ * A NULL prior hash (first run) counts as changed — the first observation of a
118
+ * monitored value is itself news.
119
+ */
120
+ export function EvaluateNotifyCondition(condition, runStatus, resultHash, priorResultHash) {
121
+ if (runStatus !== 'Success' && runStatus !== 'Failed') {
122
+ return false;
123
+ }
124
+ switch (condition) {
125
+ case 'Always':
126
+ return true;
127
+ case 'OnSuccess':
128
+ return runStatus === 'Success';
129
+ case 'OnFailure':
130
+ return runStatus === 'Failed';
131
+ case 'OnChange':
132
+ return resultHash != null && resultHash !== priorResultHash;
133
+ default:
134
+ // Future CHECK-constraint values flow through the generated union; stay total
135
+ // and conservative (no notification) until explicitly handled.
136
+ return false;
137
+ }
138
+ }
139
+ /**
140
+ * Returns a new array of recipients ordered by ascending Sequence. Ties preserve the
141
+ * input order (stable). The input array is not mutated.
142
+ */
143
+ export function SortRecipientsBySequence(recipients) {
144
+ return [...recipients].sort((a, b) => a.Sequence - b.Sequence);
145
+ }
146
+ /**
147
+ * Run `worker` over `items` with at most `limit` concurrent executions. Results are
148
+ * returned in input order. The worker is responsible for its own error handling — a
149
+ * rejection from one item propagates, so dispatcher callers wrap each routine's work in
150
+ * its own try/catch (per-routine error isolation).
151
+ */
152
+ export async function RunWithBoundedConcurrency(items, limit, worker) {
153
+ const effectiveLimit = Math.max(1, Math.floor(limit));
154
+ const results = new Array(items.length);
155
+ let nextIndex = 0;
156
+ const lane = async () => {
157
+ while (true) {
158
+ const index = nextIndex++;
159
+ if (index >= items.length) {
160
+ return;
161
+ }
162
+ results[index] = await worker(items[index]);
163
+ }
164
+ };
165
+ const lanes = Array.from({ length: Math.min(effectiveLimit, items.length) }, () => lane());
166
+ await Promise.all(lanes);
167
+ return results;
168
+ }
169
+ //# sourceMappingURL=UserRoutineProcessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserRoutineProcessor.js","sourceRoot":"","sources":["../src/UserRoutineProcessor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @fileoverview Driver for the User Routine Dispatcher scheduled job (P1.5).
3
+ *
4
+ * One admin-owned Scheduled Job of this type (seeded via metadata, 1-minute cron) sweeps
5
+ * `MJ: User Routines` for due routines, claims each by advancing NextRunAt BEFORE running
6
+ * (so an overlapping pass never double-runs it), executes the routine's target (Agent /
7
+ * Action / Prompt) with bounded concurrency and per-routine error isolation, records each
8
+ * execution in `MJ: User Routine Runs` (telemetry lives on the linked AgentRun / PromptRun /
9
+ * ActionExecutionLog — never duplicated), and notifies the owner + recipients per the
10
+ * routine's NotifyCondition through the MJ template + notification stack.
11
+ *
12
+ * @module @memberjunction/scheduling-engine
13
+ */
14
+ import { ValidationResult, UserInfo } from '@memberjunction/core';
15
+ import { MJScheduledJobEntity, MJUserRoutineEntity } from '@memberjunction/core-entities';
16
+ import { ScheduledJobResult, NotificationContent, ScheduledJobConfiguration } from '@memberjunction/scheduling-base-types';
17
+ import { BaseScheduledJob, ScheduledJobExecutionContext } from '../BaseScheduledJob.js';
18
+ /**
19
+ * Optional configuration (stored in ScheduledJob.Configuration). All fields optional —
20
+ * the dispatcher runs with sensible defaults when Configuration is empty.
21
+ */
22
+ export interface UserRoutineDispatcherConfiguration extends ScheduledJobConfiguration {
23
+ /** Maximum routines executed concurrently within one sweep. Default 3. */
24
+ MaxConcurrentRoutines?: number;
25
+ }
26
+ /**
27
+ * Driver for the User Routine Dispatcher scheduled job.
28
+ *
29
+ * Configuration schema (stored in ScheduledJob.Configuration):
30
+ * `{ MaxConcurrentRoutines?: number }`
31
+ *
32
+ * Execution result details (stored in ScheduledJobRun.Details):
33
+ * `{ RoutinesEvaluated, RoutinesSeeded, RoutinesRun, Succeeded, Failed, Notified }`
34
+ */
35
+ export declare class UserRoutineDispatcherDriver extends BaseScheduledJob {
36
+ /**
37
+ * The dispatcher is a by-design 1-minute sweeper: each pass claims due routines and
38
+ * does bounded work, so the engine's high-frequency cron warning doesn't apply.
39
+ */
40
+ get IsHighFrequencyByDesign(): boolean;
41
+ Execute(context: ScheduledJobExecutionContext): Promise<ScheduledJobResult>;
42
+ ValidateConfiguration(schedule: MJScheduledJobEntity): ValidationResult;
43
+ FormatNotification(context: ScheduledJobExecutionContext, result: ScheduledJobResult): NotificationContent;
44
+ /** Tolerant parse — the dispatcher needs no configuration, so empty/missing/invalid JSON yields defaults. */
45
+ private parseDispatcherConfiguration;
46
+ /**
47
+ * Load Active routines that are inside their activation window and either due or in
48
+ * need of NextRunAt seeding. The SQL prefilter narrows the sweep; JS re-verifies.
49
+ */
50
+ private loadCandidateRoutines;
51
+ /** Compute + persist NextRunAt for a never-scheduled routine WITHOUT running it. */
52
+ private seedNextRunAt;
53
+ /**
54
+ * Claim a due routine by advancing NextRunAt to the next cron occurrence BEFORE running.
55
+ * Persisting the claim first means an overlapping dispatcher pass (or a crash mid-run)
56
+ * can never double-run this occurrence — the routine simply isn't due anymore. Cross-
57
+ * process exclusion of whole sweeps is additionally provided by the Scheduled Job lock
58
+ * (the dispatcher job runs with ConcurrencyMode='Skip').
59
+ *
60
+ * @returns true when the claim persisted and the routine should run on this pass.
61
+ */
62
+ private claimRoutine;
63
+ /**
64
+ * Execute one claimed routine end-to-end: run row → target execution → run/routine
65
+ * bookkeeping → notification decision + delivery. Never throws for target failures —
66
+ * those are recorded on the run row; only run-row creation failures propagate.
67
+ */
68
+ private executeRoutine;
69
+ /** Create the `MJ: User Routine Runs` row in its initial Running state. */
70
+ private createRunRow;
71
+ /** Persist the run's terminal state + linkage. Telemetry stays on the linked records. */
72
+ private finalizeRunRow;
73
+ /** Roll the run outcome up onto the routine (LastRunAt / LastRunStatus / LastResultHash). */
74
+ private updateRoutineAfterRun;
75
+ /** Compact, capped text describing the outcome — the run row's human-readable summary. */
76
+ private buildResultSummary;
77
+ /**
78
+ * Dispatch by target type. The parameter type is derived from the generated entity so
79
+ * a future CHECK-constraint widening surfaces here at compile time.
80
+ */
81
+ private executeTarget;
82
+ /**
83
+ * Run an Agent target via AgentRunner, threading StartingPayload + RequestedSkillIDs.
84
+ *
85
+ * When the routine's dedicated conversation is available (existing `ConversationID`,
86
+ * or creatable — see {@link EnsureRoutineConversation}), the run goes through
87
+ * `RunAgentInConversation` so it lands as a proper conversation turn: a user
88
+ * ConversationDetail carrying InitialMessage, an assistant ConversationDetail with the
89
+ * agent result, and the AIAgentRun stamped with ConversationID/ConversationDetailID.
90
+ * When no conversation can be resolved, the run falls back to standalone `RunAgent`
91
+ * — identical outcome recording, just no conversation thread.
92
+ */
93
+ private executeAgentTarget;
94
+ /**
95
+ * Resolves (or lazily creates) the routine's dedicated conversation. Public so the
96
+ * integration suite can exercise the creation/reuse contract without an LLM call.
97
+ *
98
+ * The conversation is owned by the routine's owner and created with
99
+ * `ApplicationScope='Application'` + the "${ROUTINES_APPLICATION_NAME}" Application's ID,
100
+ * which keeps it OUT of the default chat list (the same hide mechanism meeting-room and
101
+ * Form Builder cockpit conversations use) while remaining fully reachable from the
102
+ * routine's UI. It is also Linked to the routine record (LinkedEntityID/LinkedRecordID)
103
+ * and pins the routine's agent as DefaultAgentID.
104
+ *
105
+ * Best-effort by design: any resolution/creation failure logs and returns null so the
106
+ * run proceeds standalone — a missing Routines app must never break a scheduled run.
107
+ */
108
+ EnsureRoutineConversation(routine: MJUserRoutineEntity, owner: UserInfo): Promise<string | null>;
109
+ /** The "${ROUTINES_APPLICATION_NAME}" Application row whose scope hides routine conversations from the default chat list. */
110
+ private findRoutinesApplication;
111
+ /** Parse RequestedSkillIDs (JSON array of AISkill IDs) — invalid/non-array content is ignored with a log. */
112
+ private parseRequestedSkillIDs;
113
+ /** Run a Prompt target via AIPromptRunner, passing StartingPayload as the data context. */
114
+ private executePromptTarget;
115
+ /** Run an Action target via ActionEngineServer; StartingPayload maps to input params by name. */
116
+ private executeActionTarget;
117
+ /** Map the routine's StartingPayload JSON object to ActionParam inputs (key → param name). */
118
+ private buildActionParams;
119
+ /**
120
+ * Deliver notifications for a run to the routine's owner + recipients (Sequence order),
121
+ * honoring per-recipient Channel and the routine's channel toggles. In-app goes through
122
+ * the standard NotificationEngine (metadata-seeded 'User Routine' type) with a raw
123
+ * `MJ: User Notifications` fallback; email is a structured TODO (see queueEmailDelivery).
124
+ *
125
+ * Best-effort: any delivery error is logged, never propagated into the run outcome.
126
+ *
127
+ * @returns true when at least one notification was actually delivered.
128
+ */
129
+ private sendRoutineNotifications;
130
+ /** Load the routine's recipients (ordering applied in JS via SortRecipientsBySequence). */
131
+ private loadRecipients;
132
+ /**
133
+ * Render the notification body through the MJ template stack: the routine's own
134
+ * NotificationTemplateID when set, else the metadata-seeded default template resolved
135
+ * BY NAME. Falls back to a plain-text body when no template resolves or rendering fails.
136
+ */
137
+ private renderNotificationMessage;
138
+ /** Resolve the routine's template (by ID) or the seeded default (by name) from the template cache. */
139
+ private resolveNotificationTemplate;
140
+ /** Plain-text notification body used when no template is resolvable. */
141
+ private buildPlainTextMessage;
142
+ /**
143
+ * In-app delivery: the standard NotificationEngine path first (respects the seeded
144
+ * 'User Routine' type + per-user preferences); when the engine cannot deliver (e.g. the
145
+ * type is not seeded in this instance), fall back to a raw `MJ: User Notifications` row
146
+ * — the same pattern used by TaskOrchestrator / shareNotification's default dispatch.
147
+ *
148
+ * @returns true when a notification record was actually created (a user who opted out
149
+ * via preferences yields false WITHOUT triggering the raw fallback).
150
+ */
151
+ private deliverInApp;
152
+ /** Raw `MJ: User Notifications` insert — the minimal, always-available in-app path. */
153
+ private deliverInAppRaw;
154
+ /**
155
+ * TODO(email delivery): wire this through the Communication framework
156
+ * (`@memberjunction/communication-engine` — see NotificationEngine.sendEmail for the
157
+ * template + provider pattern). Email delivery needs a configured communication provider
158
+ * plus recipient-address resolution (UserID → user email, or the recipient row's Email),
159
+ * so it is deliberately left as a structured seam rather than half-implemented. In-app
160
+ * delivery is fully functional; this method only logs the gap.
161
+ */
162
+ private queueEmailDelivery;
163
+ }
164
+ //# sourceMappingURL=UserRoutineDispatcherDriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserRoutineDispatcherDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/UserRoutineDispatcherDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EACH,gBAAgB,EAGhB,QAAQ,EAIX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGH,oBAAoB,EACpB,mBAAmB,EAKtB,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AAC3H,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrF;;;GAGG;AACH,MAAM,WAAW,kCAAmC,SAAQ,yBAAyB;IACjF,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAClC;AAmCD;;;;;;;;GAQG;AACH,qBACa,2BAA4B,SAAQ,gBAAgB;IAC7D;;;OAGG;IACH,IAAoB,uBAAuB,IAAI,OAAO,CAErD;IAEY,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmDjF,qBAAqB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,gBAAgB;IAgBvE,kBAAkB,CAAC,OAAO,EAAE,4BAA4B,EAAE,MAAM,EAAE,kBAAkB,GAAG,mBAAmB;IAwBjH,6GAA6G;IAC7G,OAAO,CAAC,4BAA4B;IAOpC;;;OAGG;YACW,qBAAqB;IAcnC,oFAAoF;YACtE,aAAa;IAc3B;;;;;;;;OAQG;YACW,YAAY;IAmB1B;;;;OAIG;YACW,cAAc;IA6C5B,2EAA2E;YAC7D,YAAY;IAkB1B,yFAAyF;YAC3E,cAAc;IAoB5B,6FAA6F;YAC/E,qBAAqB;IAUnC,0FAA0F;IAC1F,OAAO,CAAC,kBAAkB;IAW1B;;;OAGG;YACW,aAAa;IAkB3B;;;;;;;;;;OAUG;YACW,kBAAkB;IAsChC;;;;;;;;;;;;;OAaG;IACU,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA2C7G,6HAA6H;YAC/G,uBAAuB;IAUrC,6GAA6G;IAC7G,OAAO,CAAC,sBAAsB;IAY9B,2FAA2F;YAC7E,mBAAmB;IAyBjC,iGAAiG;YACnF,mBAAmB;IA6BjC,8FAA8F;IAC9F,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;;;;OASG;YACW,wBAAwB;IAsDtC,2FAA2F;YAC7E,cAAc;IAe5B;;;;OAIG;YACW,yBAAyB;IAqCvC,sGAAsG;IACtG,OAAO,CAAC,2BAA2B;IAWnC,wEAAwE;IACxE,OAAO,CAAC,qBAAqB;IAS7B;;;;;;;;OAQG;YACW,YAAY;IA6B1B,uFAAuF;YACzE,eAAe;IA4B7B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;CAa7B"}