@memberjunction/scheduling-engine 6.1.0-edge.1 → 6.1.0-edge.3

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,224 @@
1
+ /**
2
+ * @fileoverview Driver for the bounded action-execution-log purge.
3
+ * @module @memberjunction/scheduling-engine
4
+ */
5
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
6
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
8
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
10
+ };
11
+ import { RegisterClass } from '@memberjunction/global';
12
+ import { BaseScheduledJob } from '../BaseScheduledJob.js';
13
+ import { LogError, Metadata, RunView, ValidationErrorInfo, ValidationErrorType, ValidationResult, } from '@memberjunction/core';
14
+ /** How many rows one run will delete before stopping, when the job does not say. */
15
+ export const DEFAULT_MAX_DELETES_PER_RUN = 5000;
16
+ /**
17
+ * Turns the distinct retention values present into the buckets a purge query needs.
18
+ *
19
+ * Bucketing rather than per-row date math is what keeps this a small number of indexable range
20
+ * queries instead of a scan: retention is stamped from the action, so the distinct set is roughly
21
+ * "one per configured action", not one per log row. Pure and exported so the arithmetic is testable
22
+ * without a clock or a database.
23
+ *
24
+ * @param now the reference instant, passed in rather than read, so a test can pin it.
25
+ */
26
+ export function BuildRetentionBuckets(retentionDays, now) {
27
+ const distinct = [...new Set(retentionDays.filter((d) => Number.isFinite(d) && d > 0))].sort((a, b) => a - b);
28
+ return distinct.map((RetentionDays) => ({
29
+ RetentionDays,
30
+ Cutoff: new Date(now.getTime() - RetentionDays * 24 * 60 * 60 * 1000),
31
+ }));
32
+ }
33
+ /**
34
+ * Purges expired `MJ: Action Execution Logs` rows, bounded per run.
35
+ *
36
+ * **Retention is decided at write time, not here.** `StartActionLog` stamps each row's
37
+ * `RetentionPeriod` from its action's, so this job reads only the row's own value — which is what
38
+ * makes an edit to `Action.RetentionPeriod` a going-forward change rather than a retroactive
39
+ * deletion of history written under the previous policy. A row with NULL retention is kept forever
40
+ * unless the job is explicitly configured with `DefaultRetentionDays`.
41
+ *
42
+ * **Deletion goes through `BaseEntity.Delete()`, one row at a time.** A `DELETE` statement would be
43
+ * faster and is a sanctioned use of raw DML in principle — but only for an entity that has opted in
44
+ * via `AllowDirectSQLDelete`, and this one has not. Honouring the platform's own gate matters more
45
+ * here than throughput, because the job is bounded anyway: the cap exists so a first run against a
46
+ * long-neglected table cannot become an unbounded transaction, and raising it is a configuration
47
+ * change rather than a code change.
48
+ *
49
+ * Opt-in like every other maintenance driver: shipping the job type activates nothing until someone
50
+ * creates a `MJ: Scheduled Job` of this type with a cron expression.
51
+ */
52
+ let ActionLogRetentionScheduledJobDriver = class ActionLogRetentionScheduledJobDriver extends BaseScheduledJob {
53
+ async Execute(context) {
54
+ const config = this.readConfiguration(context.Schedule);
55
+ const maxDeletes = config.MaxDeletesPerRun ?? DEFAULT_MAX_DELETES_PER_RUN;
56
+ try {
57
+ const expired = await this.findExpiredLogs(config, maxDeletes, context.ContextUser);
58
+ const { Deleted, Failed } = await this.deleteBounded(expired);
59
+ return {
60
+ Success: true,
61
+ Details: {
62
+ Deleted,
63
+ Failed,
64
+ Examined: expired.length,
65
+ // Says plainly that the run stopped at its ceiling rather than because it was
66
+ // finished — otherwise "Deleted: 5000" reads as "all clean" when it is not.
67
+ ReachedCap: expired.length >= maxDeletes,
68
+ },
69
+ };
70
+ }
71
+ catch (e) {
72
+ return { Success: false, ErrorMessage: `Action-log purge failed: ${e instanceof Error ? e.message : String(e)}` };
73
+ }
74
+ }
75
+ /**
76
+ * The expired rows, oldest first, capped at `maxDeletes`.
77
+ *
78
+ * Oldest-first is not cosmetic: a capped run that took an arbitrary slice would leave the same
79
+ * ancient rows behind every night while deleting newer ones, so the backlog would never drain.
80
+ */
81
+ async findExpiredLogs(config, maxDeletes, user) {
82
+ const filter = await this.buildExpiredFilter(config, user);
83
+ if (!filter) {
84
+ return [];
85
+ }
86
+ // A scheduled maintenance sweep is a server-global task, so the global default provider is
87
+ // the correct source here (not a per-request/per-tenant provider).
88
+ const rv = RunView.FromMetadataProvider(Metadata.Provider); // global-provider-ok: scheduled maintenance sweep is a server-global task, not per-request/per-tenant
89
+ const result = await rv.RunView({
90
+ EntityName: 'MJ: Action Execution Logs',
91
+ ExtraFilter: filter,
92
+ OrderBy: 'StartedAt ASC',
93
+ MaxRows: maxDeletes,
94
+ ResultType: 'entity_object',
95
+ }, user);
96
+ if (!result.Success) {
97
+ throw new Error(result.ErrorMessage ?? 'could not read the action execution log');
98
+ }
99
+ return result.Results ?? [];
100
+ }
101
+ /**
102
+ * The WHERE clause selecting expired rows, or null when nothing can expire.
103
+ *
104
+ * Returning null rather than a clause that matches nothing is deliberate — it lets the caller
105
+ * skip the query entirely on an instance where no action has ever configured retention, which is
106
+ * the common case and should cost nothing nightly.
107
+ */
108
+ async buildExpiredFilter(config, user) {
109
+ const now = new Date();
110
+ const clauses = [];
111
+ const buckets = BuildRetentionBuckets(await this.distinctRetentionDays(user), now);
112
+ for (const bucket of buckets) {
113
+ clauses.push(`(RetentionPeriod = ${bucket.RetentionDays} AND StartedAt < '${bucket.Cutoff.toISOString()}')`);
114
+ }
115
+ const fallback = config.DefaultRetentionDays;
116
+ if (fallback != null && Number.isFinite(fallback) && fallback > 0) {
117
+ const cutoff = new Date(now.getTime() - fallback * 24 * 60 * 60 * 1000);
118
+ clauses.push(`(RetentionPeriod IS NULL AND StartedAt < '${cutoff.toISOString()}')`);
119
+ }
120
+ return clauses.length > 0 ? clauses.join(' OR ') : null;
121
+ }
122
+ /**
123
+ * The retention lifetimes actually in use, read from the actions that stamped them.
124
+ *
125
+ * Read from `MJ: Actions` rather than by scanning the log for distinct values, because the log
126
+ * is the large table and the actions table is not — and the two agree by construction, since the
127
+ * stamp comes from here.
128
+ */
129
+ async distinctRetentionDays(user) {
130
+ const rv = RunView.FromMetadataProvider(Metadata.Provider); // global-provider-ok: scheduled maintenance sweep is a server-global task, not per-request/per-tenant
131
+ const result = await rv.RunView({
132
+ EntityName: 'MJ: Actions',
133
+ ExtraFilter: 'RetentionPeriod IS NOT NULL',
134
+ Fields: ['RetentionPeriod'],
135
+ ResultType: 'simple',
136
+ }, user);
137
+ if (!result.Success) {
138
+ throw new Error(result.ErrorMessage ?? 'could not read action retention settings');
139
+ }
140
+ return (result.Results ?? []).map((r) => Number(r.RetentionPeriod)).filter((d) => Number.isFinite(d));
141
+ }
142
+ /**
143
+ * Deletes the given rows, counting rather than aborting on a failure.
144
+ *
145
+ * One bad row — a FK an extension added, a permission gap — must not strand the rest of the
146
+ * purge behind it every night. The count is reported so the failure is visible.
147
+ */
148
+ async deleteBounded(rows) {
149
+ let Deleted = 0;
150
+ let Failed = 0;
151
+ for (const row of rows) {
152
+ if (await row.Delete()) {
153
+ Deleted++;
154
+ }
155
+ else {
156
+ Failed++;
157
+ LogError(`ActionLogRetention: could not delete action execution log ${row.ID}: ` +
158
+ `${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
159
+ }
160
+ }
161
+ return { Deleted, Failed };
162
+ }
163
+ ValidateConfiguration(schedule) {
164
+ const result = new ValidationResult();
165
+ const config = this.readConfiguration(schedule);
166
+ for (const [field, value] of [
167
+ ['DefaultRetentionDays', config.DefaultRetentionDays],
168
+ ['MaxDeletesPerRun', config.MaxDeletesPerRun],
169
+ ]) {
170
+ if (value != null && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
171
+ result.Errors.push(new ValidationErrorInfo(`Configuration.${field}`, `${field} must be a positive number when provided`, value, ValidationErrorType.Failure));
172
+ }
173
+ }
174
+ result.Success = result.Errors.length === 0;
175
+ return result;
176
+ }
177
+ FormatNotification(context, result) {
178
+ const deleted = result.Details?.['Deleted'] ?? 0;
179
+ const failed = result.Details?.['Failed'] ?? 0;
180
+ const reachedCap = result.Details?.['ReachedCap'] ?? false;
181
+ if (!result.Success) {
182
+ return {
183
+ Subject: `Action-log purge failed: ${context.Schedule.Name}`,
184
+ Body: `The scheduled action-log purge "${context.Schedule.Name}" failed.\n\nError: ${result.ErrorMessage ?? 'unknown'}`,
185
+ Priority: 'High',
186
+ Metadata: { Deleted: deleted, Failed: failed },
187
+ };
188
+ }
189
+ const capNote = reachedCap
190
+ ? '\n\nThe run stopped at its per-run ceiling, so expired rows remain — it will continue on the next run.'
191
+ : '';
192
+ return {
193
+ Subject: `Action-log purge: ${deleted} expired row(s) removed`,
194
+ Body: `The scheduled action-log purge "${context.Schedule.Name}" removed ${deleted} expired log row(s)`
195
+ + `${failed > 0 ? `, and could not remove ${failed}` : ''}.${capNote}`,
196
+ Priority: failed > 0 ? 'High' : deleted > 0 ? 'Normal' : 'Low',
197
+ Metadata: { Deleted: deleted, Failed: failed, ReachedCap: reachedCap },
198
+ };
199
+ }
200
+ /**
201
+ * Tolerant parse — the purge needs no configuration, so empty/missing JSON yields defaults.
202
+ *
203
+ * Named apart from the base's `parseConfiguration`, which throws on an empty Configuration
204
+ * because most job types genuinely require one. This job's every setting is optional, and a job
205
+ * that refused to run because nobody filled in a box would be a retention policy that quietly
206
+ * never applies.
207
+ */
208
+ readConfiguration(schedule) {
209
+ if (!schedule.Configuration) {
210
+ return {};
211
+ }
212
+ try {
213
+ return JSON.parse(schedule.Configuration);
214
+ }
215
+ catch {
216
+ return {};
217
+ }
218
+ }
219
+ };
220
+ ActionLogRetentionScheduledJobDriver = __decorate([
221
+ RegisterClass(BaseScheduledJob, 'ActionLogRetentionScheduledJobDriver')
222
+ ], ActionLogRetentionScheduledJobDriver);
223
+ export { ActionLogRetentionScheduledJobDriver };
224
+ //# sourceMappingURL=ActionLogRetentionScheduledJobDriver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionLogRetentionScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/ActionLogRetentionScheduledJobDriver.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AgentScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAmF,MAAM,sBAAsB,CAAC;AAGzI,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EAEtB,MAAM,uCAAuC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,uBAAwB,SAAQ,gBAAgB;IAC5C,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmDjF,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,gBAAgB;IA4CtD,kBAAkB,CACrB,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,kBAAkB,GAC3B,mBAAmB;YA8BR,SAAS;CAW1B"}
1
+ {"version":3,"file":"AgentScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAmF,MAAM,sBAAsB,CAAC;AAGzI,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EAEtB,MAAM,uCAAuC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,uBAAwB,SAAQ,gBAAgB;IAC5C,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAqEjF,qBAAqB,CAAC,QAAQ,EAAE,GAAG,GAAG,gBAAgB;IA4CtD,kBAAkB,CACrB,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,kBAAkB,GAC3B,mBAAmB;YA8BR,SAAS;CAW1B"}
@@ -39,6 +39,22 @@ let AgentScheduledJobDriver = class AgentScheduledJobDriver extends BaseSchedule
39
39
  const config = this.parseConfiguration(context.Schedule);
40
40
  // Load the agent entity
41
41
  const agent = await this.loadAgent(config.AgentID, context.ContextUser);
42
+ // A workflow SUBMITS and detaches — it returns a handle as soon as its steps are scheduled,
43
+ // not a result. A scheduled job that ran one would record success the moment the graph was
44
+ // written, before any of the work happened: the job history would show a clean run every
45
+ // time regardless of whether the workflow later succeeded, failed or stalled. That is worse
46
+ // than not running it, because it looks like it worked.
47
+ //
48
+ // Refused here rather than at submission because this is where the caller's expectation
49
+ // lives: the job expects a completed unit of work, and nothing downstream of this point can
50
+ // supply one until an await path exists.
51
+ if ((agent.Type ?? '').trim().toLowerCase() === 'flow') {
52
+ const message = `'${agent.Name}' is a workflow and cannot be run on a schedule yet. A workflow returns ` +
53
+ `as soon as its steps are scheduled, so this job would report success before any of the ` +
54
+ `work had happened. Schedule the steps individually, or start the workflow by hand.`;
55
+ this.log(message);
56
+ return { Success: false, ErrorMessage: message, Details: { AgentID: config.AgentID } };
57
+ }
42
58
  // Verbose-only: the engine's always-on "▶️ Starting / ✅ Completed" pair already frames this
43
59
  // run. This per-agent detail is redundant noise on routine (often no-op) scheduled runs.
44
60
  this.log(`Executing agent: ${agent.Name}`, true);
@@ -1 +1 @@
1
- {"version":3,"file":"AgentScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AgentScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/AgentScheduledJobDriver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @fileoverview Scheduled-job driver that refreshes due materialized results on a cron schedule —
3
+ * sibling to RecordProcessScheduledJobDriver / ActionScheduledJobDriver. Delegates the actual
4
+ * rebuild to the @memberjunction/materialization engine.
5
+ * @module @memberjunction/scheduling-engine
6
+ */
7
+ import { ValidationResult } from '@memberjunction/core';
8
+ import { BaseScheduledJob, ScheduledJobExecutionContext } from '../BaseScheduledJob.js';
9
+ import { ScheduledJobResult, NotificationContent } from '@memberjunction/scheduling-base-types';
10
+ /**
11
+ * Driver for the materialization refresh sweep (materialization plan §11.1).
12
+ *
13
+ * Unlike most drivers, this one has **no per-job configuration** — it processes the entire due queue:
14
+ * every non-disabled, scheduled `MJ: Materialized Results` row whose `NextRefreshAt` is at/before now
15
+ * (or null). Each is refreshed via the engine (full rebuild + atomic wrapper-view swap) and its
16
+ * `NextRefreshAt` is advanced from its own `RefreshSchedule` cron. Per-materialization cadence lives
17
+ * on the rows; this job just needs to run often enough to honor the smallest cadence.
18
+ */
19
+ export declare class MaterializationRefreshScheduledJobDriver extends BaseScheduledJob {
20
+ Execute(context: ScheduledJobExecutionContext): Promise<ScheduledJobResult>;
21
+ /** No per-job configuration — the driver always sweeps the whole due queue. */
22
+ ValidateConfiguration(): ValidationResult;
23
+ FormatNotification(_context: ScheduledJobExecutionContext, result: ScheduledJobResult): NotificationContent;
24
+ }
25
+ //# sourceMappingURL=MaterializationRefreshScheduledJobDriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MaterializationRefreshScheduledJobDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/MaterializationRefreshScheduledJobDriver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,sBAAsB,CAAC;AAG9F,OAAO,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAGhG;;;;;;;;GAQG;AACH,qBACa,wCAAyC,SAAQ,gBAAgB;IAC7D,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmExF,+EAA+E;IACxE,qBAAqB,IAAI,gBAAgB;IAIzC,kBAAkB,CAAC,QAAQ,EAAE,4BAA4B,EAAE,MAAM,EAAE,kBAAkB,GAAG,mBAAmB;CAQrH"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @fileoverview Scheduled-job driver that refreshes due materialized results on a cron schedule —
3
+ * sibling to RecordProcessScheduledJobDriver / ActionScheduledJobDriver. Delegates the actual
4
+ * rebuild to the @memberjunction/materialization engine.
5
+ * @module @memberjunction/scheduling-engine
6
+ */
7
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
8
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
10
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
11
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
12
+ };
13
+ import { RegisterClass } from '@memberjunction/global';
14
+ import { ValidationResult, RunView, Metadata } from '@memberjunction/core';
15
+ import { MaterializationRefresher } from '@memberjunction/materialization';
16
+ import { BaseScheduledJob } from '../BaseScheduledJob.js';
17
+ import { CronExpressionHelper } from '../CronExpressionHelper.js';
18
+ /**
19
+ * Driver for the materialization refresh sweep (materialization plan §11.1).
20
+ *
21
+ * Unlike most drivers, this one has **no per-job configuration** — it processes the entire due queue:
22
+ * every non-disabled, scheduled `MJ: Materialized Results` row whose `NextRefreshAt` is at/before now
23
+ * (or null). Each is refreshed via the engine (full rebuild + atomic wrapper-view swap) and its
24
+ * `NextRefreshAt` is advanced from its own `RefreshSchedule` cron. Per-materialization cadence lives
25
+ * on the rows; this job just needs to run often enough to honor the smallest cadence.
26
+ */
27
+ let MaterializationRefreshScheduledJobDriver = class MaterializationRefreshScheduledJobDriver extends BaseScheduledJob {
28
+ async Execute(context) {
29
+ const provider = Metadata.Provider; // global-provider-ok: scheduled job runs in the server's single default-provider context
30
+ // Candidate queue: scheduled materializations that are neither disabled nor held for drift
31
+ // review (§13/§17.2 — DriftHold rows stop refreshing until a human resolves the drift).
32
+ // Due-selection is done in JS (engine.filterDue) so the date comparison stays engine-agnostic.
33
+ const rv = new RunView();
34
+ const candidates = await rv.RunView({
35
+ EntityName: 'MJ: Materialized Results',
36
+ ExtraFilter: `Status NOT IN ('Disabled', 'DriftHold') AND RefreshSchedule IS NOT NULL`,
37
+ ResultType: 'entity_object',
38
+ }, context.ContextUser);
39
+ // RunView returns Success:false (never throws) on failure. Without this check a failed candidate
40
+ // load yields an empty due set and the sweep reports GREEN while silently refreshing nothing —
41
+ // masking a total refresh outage. Surface it as a job failure instead.
42
+ if (!candidates.Success) {
43
+ return {
44
+ Success: false,
45
+ ErrorMessage: `Failed to load due materializations: ${candidates.ErrorMessage ?? 'unknown error'}`,
46
+ Details: { DueCount: 0, Refreshed: 0, Failed: 0, Items: [] },
47
+ };
48
+ }
49
+ const now = new Date();
50
+ const due = MaterializationRefresher.filterDue(candidates.Results ?? [], now);
51
+ const refresher = new MaterializationRefresher();
52
+ let refreshed = 0;
53
+ let failed = 0;
54
+ const items = [];
55
+ for (const mr of due) {
56
+ void context.heartbeat?.();
57
+ // Advance NextRefreshAt from this row's own cron (UTC). Candidates are pre-filtered to
58
+ // RefreshSchedule IS NOT NULL, so a throw here means a MALFORMED cron. In that case we must
59
+ // NOT refresh: RefreshOne would set NextRefreshAt=null, which filterDue treats as due, so the
60
+ // expensive full-rebuild-with-swap would run on EVERY sweep forever. Instead surface it as a
61
+ // failed item and skip — the row stays selected but untouched until the cron is corrected.
62
+ let nextRefreshAt;
63
+ try {
64
+ nextRefreshAt = CronExpressionHelper.GetNextRunTime(mr.RefreshSchedule, 'UTC', now);
65
+ }
66
+ catch {
67
+ failed++;
68
+ items.push({ ID: mr.ID, TableName: mr.TableName, Success: false, Error: `Invalid RefreshSchedule cron "${mr.RefreshSchedule}" — skipped (not refreshed) until corrected.` });
69
+ continue;
70
+ }
71
+ const result = await refresher.RefreshOne(mr, context.ContextUser, provider, { nextRefreshAt });
72
+ if (result.Success) {
73
+ refreshed++;
74
+ }
75
+ else {
76
+ failed++;
77
+ }
78
+ items.push({ ID: mr.ID, TableName: mr.TableName, Success: result.Success, RowCount: result.RowCount, Error: result.ErrorMessage });
79
+ }
80
+ return {
81
+ Success: failed === 0,
82
+ ErrorMessage: failed > 0 ? `${failed} materialization(s) failed to refresh` : undefined,
83
+ Details: { DueCount: due.length, Refreshed: refreshed, Failed: failed, Items: items },
84
+ };
85
+ }
86
+ /** No per-job configuration — the driver always sweeps the whole due queue. */
87
+ ValidateConfiguration() {
88
+ return new ValidationResult();
89
+ }
90
+ FormatNotification(_context, result) {
91
+ const d = result.Details ?? {};
92
+ return {
93
+ Subject: `Materialization refresh — ${result.Success ? 'OK' : 'errors'}`,
94
+ Body: `Refreshed ${d.Refreshed ?? 0} of ${d.DueCount ?? 0} due materialization(s)${(d.Failed ?? 0) > 0 ? `; ${d.Failed} failed` : ''}.`,
95
+ Priority: result.Success ? 'Normal' : 'High',
96
+ };
97
+ }
98
+ };
99
+ MaterializationRefreshScheduledJobDriver = __decorate([
100
+ RegisterClass(BaseScheduledJob, 'MaterializationRefreshScheduledJobDriver')
101
+ ], MaterializationRefreshScheduledJobDriver);
102
+ export { MaterializationRefreshScheduledJobDriver };
103
+ //# sourceMappingURL=MaterializationRefreshScheduledJobDriver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MaterializationRefreshScheduledJobDriver.js","sourceRoot":"","sources":["../../src/drivers/MaterializationRefreshScheduledJobDriver.ts"],"names":[],"mappings":"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"}
@@ -64,8 +64,18 @@ export declare class UserRoutineDispatcherDriver extends BaseScheduledJob {
64
64
  * Execute one claimed routine end-to-end: run row → target execution → run/routine
65
65
  * bookkeeping → notification decision + delivery. Never throws for target failures —
66
66
  * those are recorded on the run row; only run-row creation failures propagate.
67
+ *
68
+ * **Once the run row exists, this drives it to a TERMINAL status on every path.** The inner
69
+ * guard around `executeTarget` only ever covered target failures, so a throw from the
70
+ * bookkeeping that follows it — `finalizeRunRow`, `updateRoutineAfterRun`, notification
71
+ * delivery — escaped to the sweep's per-routine catch, which records `Failed` in its in-memory
72
+ * summary and returns. The row on disk stays `Running` forever: nothing reconciles it, the
73
+ * sweep reports success, and the routine looks like it is still executing long after the
74
+ * process that owned it exited. A run row that outlives its executor has to say so.
67
75
  */
68
76
  private executeRoutine;
77
+ /** The body of {@link executeRoutine}, from target execution through notification delivery. */
78
+ private runAndRecord;
69
79
  /** Create the `MJ: User Routine Runs` row in its initial Running state. */
70
80
  private createRunRow;
71
81
  /** Persist the run's terminal state + linkage. Telemetry stays on the linked records. */
@@ -1 +1 @@
1
- {"version":3,"file":"UserRoutineDispatcherDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/UserRoutineDispatcherDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAkD,QAAQ,EAA4C,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC5J,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"}
1
+ {"version":3,"file":"UserRoutineDispatcherDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/UserRoutineDispatcherDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAkD,QAAQ,EAA4C,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC5J,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;AAyED;;;;;;;;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;;;;;;;;;;;;OAYG;YACW,cAAc;IAiD5B,+FAA+F;YACjF,YAAY;IA4C1B,2EAA2E;YAC7D,YAAY;IAkB1B,yFAAyF;YAC3E,cAAc;IA0E5B,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"}
@@ -28,8 +28,44 @@ import { TemplateEngineServer } from '@memberjunction/templates';
28
28
  import { NotificationEngine } from '@memberjunction/notifications';
29
29
  import { BaseScheduledJob } from '../BaseScheduledJob.js';
30
30
  import { ComputeRoutineNextRunAt, ComputeResultHash, EvaluateNotifyCondition, IsRoutineDue, RoutineNeedsSeeding, BuildDueRoutineFilter, SortRecipientsBySequence, RunWithBoundedConcurrency, } from '../UserRoutineProcessor.js';
31
+ /**
32
+ * Attempts to re-attach a run's execution linkage after the first save was rejected.
33
+ *
34
+ * The linkage columns are foreign keys to rows written fire-and-forget by the action/agent/prompt
35
+ * engines, so a rejection usually means "not inserted yet" rather than "does not exist". A few
36
+ * short, escalating waits cover that window without putting the sweep on the DB's critical path.
37
+ */
38
+ const RUN_LINKAGE_ATTEMPTS = 3;
39
+ const RUN_LINKAGE_RETRY_MS = 50;
31
40
  /** Default bound on concurrent routine executions within a single dispatcher sweep. */
32
41
  const DEFAULT_MAX_CONCURRENT_ROUTINES = 3;
42
+ /**
43
+ * The run's status **as last persisted** — never the in-memory value.
44
+ *
45
+ * `BaseEntity.finalizeSave` runs only when the save returned a row, and it calls
46
+ * `SetMany(data, …, replaceOldValues = true)`. So `OldValue` holds the value on disk: it advances
47
+ * to 'Success'/'Failed' when the write lands, and stays 'Running' when it does not.
48
+ *
49
+ * The distinction is the whole point. `finalizeRunRow` assigns `Status = 'Success'` BEFORE its
50
+ * first save, so after a rejected save the in-memory `Status` says 'Success' while the row still
51
+ * says 'Running'. A caller that reads `run.Status` to decide "was an outcome already committed?"
52
+ * gets 'Success' and leaves the row stuck at 'Running' forever — the exact defect this module's
53
+ * tests were written to eliminate, reintroduced at the seam between the two layers that each fix
54
+ * one half of it.
55
+ *
56
+ * Falls back to the in-memory value only when the field cannot be resolved, which cannot happen
57
+ * for a real `MJUserRoutineRunEntity` (`Status` is a column); it keeps this total rather than
58
+ * throwing inside an error handler.
59
+ */
60
+ function persistedRunStatus(run) {
61
+ const persisted = run.GetFieldByName?.('Status')?.OldValue;
62
+ // `OldValue` is untyped on EntityField, so this is the one narrowing boundary. The column is
63
+ // CHECK-constrained to the same value list as the property, so a string read from it IS that
64
+ // union — asserting anything wider here would just push the cast to every caller.
65
+ return typeof persisted === 'string'
66
+ ? persisted
67
+ : run.Status;
68
+ }
33
69
  /** Name of the metadata-seeded default notification template (resolved BY NAME, never hardcoded ID). */
34
70
  const DEFAULT_NOTIFICATION_TEMPLATE_NAME = 'User Routine Notification - Default';
35
71
  /** Name of the metadata-seeded `MJ: User Notification Types` row used for in-app delivery. */
@@ -201,11 +237,57 @@ let UserRoutineDispatcherDriver = class UserRoutineDispatcherDriver extends Base
201
237
  * Execute one claimed routine end-to-end: run row → target execution → run/routine
202
238
  * bookkeeping → notification decision + delivery. Never throws for target failures —
203
239
  * those are recorded on the run row; only run-row creation failures propagate.
240
+ *
241
+ * **Once the run row exists, this drives it to a TERMINAL status on every path.** The inner
242
+ * guard around `executeTarget` only ever covered target failures, so a throw from the
243
+ * bookkeeping that follows it — `finalizeRunRow`, `updateRoutineAfterRun`, notification
244
+ * delivery — escaped to the sweep's per-routine catch, which records `Failed` in its in-memory
245
+ * summary and returns. The row on disk stays `Running` forever: nothing reconciles it, the
246
+ * sweep reports success, and the routine looks like it is still executing long after the
247
+ * process that owned it exited. A run row that outlives its executor has to say so.
204
248
  */
205
249
  async executeRoutine(routine, context) {
206
250
  const contextUser = context.ContextUser;
207
251
  const run = await this.createRunRow(routine, contextUser);
208
252
  this.log(`Running routine "${routine.Name}" (${routine.TargetType} target)`, true);
253
+ try {
254
+ return await this.runAndRecord(routine, run, context);
255
+ }
256
+ catch (error) {
257
+ const message = error instanceof Error ? error.message : String(error);
258
+ this.logError(`Routine "${routine.Name}" (${routine.ID}) failed after its run row was created`, error);
259
+ // ONLY drive a row that is still non-terminal. Everything after `finalizeRunRow` runs
260
+ // inside this scope — `updateRoutineAfterRun`, the notification save — so a throw from
261
+ // there arrives with `Status` already committed as `Success` alongside its
262
+ // ResultSummary and ResultHash, and `MJUserRoutine.LastRunStatus` possibly already
263
+ // agreeing. Overwriting it with `Failed` would rewrite a run that genuinely succeeded
264
+ // and put the run and its routine into disagreement — inventing the very status/disk
265
+ // mismatch this catch was added to eliminate.
266
+ //
267
+ // ASK THE ROW, NOT THE OBJECT. This must read the PERSISTED status, because the other
268
+ // way into this catch is `finalizeRunRow` itself throwing — and it throws having
269
+ // already set `Status = 'Success'` in memory on an object whose saves all failed.
270
+ // Reading `run.Status` there would report a committed success for a row that is still
271
+ // 'Running' on disk, which is precisely the bug the throw exists to signal.
272
+ const committed = persistedRunStatus(run);
273
+ if (committed !== 'Running') {
274
+ this.logError(`Run ${run.ID} already recorded '${committed}'; leaving it as-is. The failure above ` +
275
+ `happened in the bookkeeping AFTER the outcome was committed.`);
276
+ return { RoutineID: routine.ID, RunStatus: committed, Notified: false };
277
+ }
278
+ run.Status = 'Failed';
279
+ run.ErrorMessage = `Run bookkeeping failed: ${message}`;
280
+ run.CompletedAt = new Date();
281
+ if (!(await run.Save())) {
282
+ this.logError(`Run ${run.ID} is STUCK at Running — the failure could not be recorded: ` +
283
+ `${run.LatestResult?.CompleteMessage ?? 'unknown'}`);
284
+ }
285
+ return { RoutineID: routine.ID, RunStatus: 'Failed', Notified: false };
286
+ }
287
+ }
288
+ /** The body of {@link executeRoutine}, from target execution through notification delivery. */
289
+ async runAndRecord(routine, run, context) {
290
+ const contextUser = context.ContextUser;
209
291
  let outcome;
210
292
  try {
211
293
  outcome = await this.executeTarget(routine, contextUser, context.heartbeat);
@@ -263,10 +345,56 @@ let UserRoutineDispatcherDriver = class UserRoutineDispatcherDriver extends Base
263
345
  run.AgentRunID = outcome.AgentRunID;
264
346
  run.PromptRunID = outcome.PromptRunID;
265
347
  run.ActionExecutionLogID = outcome.ActionExecutionLogID;
266
- const saved = await run.Save();
267
- if (!saved) {
268
- this.logError(`Failed to finalize run ${run.ID}: ${run.LatestResult?.CompleteMessage ?? 'unknown'}`);
269
- }
348
+ if (await run.Save()) {
349
+ return;
350
+ }
351
+ // The run's own status must not be hostage to an OPTIONAL linkage. The three ID columns
352
+ // above are foreign keys to artifacts of the execution — the action log, the agent run, the
353
+ // prompt run — and any of them can fail to resolve (a log row cleaned up by an unrelated
354
+ // process, a cross-session write not yet visible). When that FK is what rejected the save,
355
+ // the previous behaviour logged and carried on: `Status` stayed 'Success' IN MEMORY, the
356
+ // caller reported success from that in-memory value, and the row on disk stayed 'Running'
357
+ // forever with nothing to reconcile it. Losing a link is a diagnostic loss; losing the
358
+ // status is a correctness one — so drop the links and record the outcome.
359
+ const firstFailure = run.LatestResult?.CompleteMessage ?? 'unknown';
360
+ // The usual cause is a RACE, not a bad ID. Action-execution logging is fire-and-forget:
361
+ // `ActionEngine` client-generates the log row's PK and returns it immediately, then INSERTs
362
+ // it off the critical path. So `outcome.ActionExecutionLogID` is a valid identifier for a
363
+ // row that may not exist YET — and writing it into a foreign key can lose that race, which
364
+ // is why this only shows up under load. Land the STATUS first (the correctness-bearing
365
+ // part), then re-attach the linkage once the INSERT has had a chance to land.
366
+ const linkage = {
367
+ AgentRunID: run.AgentRunID,
368
+ PromptRunID: run.PromptRunID,
369
+ ActionExecutionLogID: run.ActionExecutionLogID,
370
+ };
371
+ run.AgentRunID = null;
372
+ run.PromptRunID = null;
373
+ run.ActionExecutionLogID = null;
374
+ if (!(await run.Save())) {
375
+ throw new Error(`Run ${run.ID} could not be finalized and is stuck at 'Running': ${run.LatestResult?.CompleteMessage ?? firstFailure}`);
376
+ }
377
+ for (let attempt = 1; attempt <= RUN_LINKAGE_ATTEMPTS; attempt++) {
378
+ await new Promise((resolve) => setTimeout(resolve, RUN_LINKAGE_RETRY_MS * attempt));
379
+ run.AgentRunID = linkage.AgentRunID;
380
+ run.PromptRunID = linkage.PromptRunID;
381
+ run.ActionExecutionLogID = linkage.ActionExecutionLogID;
382
+ if (await run.Save()) {
383
+ return;
384
+ }
385
+ // Re-null so the next attempt starts from the persisted (unlinked) state rather than
386
+ // stacking a second rejected write on top of a dirty entity.
387
+ run.AgentRunID = null;
388
+ run.PromptRunID = null;
389
+ run.ActionExecutionLogID = null;
390
+ }
391
+ // Status is recorded; only the diagnostic linkage is missing. Say so and move on — losing a
392
+ // link is an observability loss, and blocking the sweep on it would be the worse trade.
393
+ run.ErrorMessage = [outcome.ErrorMessage, `execution linkage not attached: ${firstFailure}`]
394
+ .filter(Boolean).join(' | ');
395
+ const noted = await run.Save();
396
+ this.logError(`Run ${run.ID} finalized WITHOUT execution linkage after ${RUN_LINKAGE_ATTEMPTS} attempt(s): ${firstFailure}` +
397
+ (noted ? '' : ` — and the note could not be written: ${run.LatestResult?.CompleteMessage ?? 'unknown'}`));
270
398
  }
271
399
  /** Roll the run outcome up onto the routine (LastRunAt / LastRunStatus / LastResultHash). */
272
400
  async updateRoutineAfterRun(routine, run) {