@memberjunction/task-graph 0.0.0 → 6.1.0-edge.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.
Files changed (43) hide show
  1. package/LICENSE +7 -0
  2. package/dist/DispatcherConditionEvaluator.d.ts +10 -0
  3. package/dist/DispatcherConditionEvaluator.d.ts.map +1 -0
  4. package/dist/DispatcherConditionEvaluator.js +27 -0
  5. package/dist/DispatcherConditionEvaluator.js.map +1 -0
  6. package/dist/TaskClaimStore.d.ts +114 -0
  7. package/dist/TaskClaimStore.d.ts.map +1 -0
  8. package/dist/TaskClaimStore.js +218 -0
  9. package/dist/TaskClaimStore.js.map +1 -0
  10. package/dist/TaskGraphDispatcher.d.ts +186 -0
  11. package/dist/TaskGraphDispatcher.d.ts.map +1 -0
  12. package/dist/TaskGraphDispatcher.js +735 -0
  13. package/dist/TaskGraphDispatcher.js.map +1 -0
  14. package/dist/TaskGraphService.d.ts +138 -0
  15. package/dist/TaskGraphService.d.ts.map +1 -0
  16. package/dist/TaskGraphService.js +314 -0
  17. package/dist/TaskGraphService.js.map +1 -0
  18. package/dist/TaskGraphSubmitterImpl.d.ts +7 -0
  19. package/dist/TaskGraphSubmitterImpl.d.ts.map +1 -0
  20. package/dist/TaskGraphSubmitterImpl.js +52 -0
  21. package/dist/TaskGraphSubmitterImpl.js.map +1 -0
  22. package/dist/WorkflowSpecSync.d.ts +171 -0
  23. package/dist/WorkflowSpecSync.d.ts.map +1 -0
  24. package/dist/WorkflowSpecSync.js +393 -0
  25. package/dist/WorkflowSpecSync.js.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +18 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/operations/TaskGraphOperations.d.ts +39 -0
  31. package/dist/operations/TaskGraphOperations.d.ts.map +1 -0
  32. package/dist/operations/TaskGraphOperations.js +164 -0
  33. package/dist/operations/TaskGraphOperations.js.map +1 -0
  34. package/dist/operations/WorkflowOperations.d.ts +22 -0
  35. package/dist/operations/WorkflowOperations.d.ts.map +1 -0
  36. package/dist/operations/WorkflowOperations.js +99 -0
  37. package/dist/operations/WorkflowOperations.js.map +1 -0
  38. package/dist/types.d.ts +214 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +9 -0
  41. package/dist/types.js.map +1 -0
  42. package/package.json +33 -8
  43. package/README.md +0 -45
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2023 MemberJunction
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,10 @@
1
+ import type { IConditionEvaluator } from '@memberjunction/ai-core-plus';
2
+ export declare class DispatcherConditionEvaluator implements IConditionEvaluator {
3
+ private readonly evaluator;
4
+ Evaluate(expression: string, context: Record<string, unknown>): {
5
+ Success: boolean;
6
+ Value?: unknown;
7
+ ErrorMessage?: string;
8
+ };
9
+ }
10
+ //# sourceMappingURL=DispatcherConditionEvaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DispatcherConditionEvaluator.d.ts","sourceRoot":"","sources":["../src/DispatcherConditionEvaluator.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAExE,qBAAa,4BAA6B,YAAW,mBAAmB;IACpE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAEpD,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE;CAUtI"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @fileoverview Condition evaluation for durable graph edges.
3
+ *
4
+ * Wraps the same `SafeExpressionEvaluator` the design-time flow executor uses. Sharing the evaluator
5
+ * is not incidental — it is what makes an edge condition mean the same thing whether it was drawn in
6
+ * the flow editor or emitted by an agent, which is the premise Save as Workflow (D17) rests on.
7
+ *
8
+ * @module @memberjunction/task-graph
9
+ */
10
+ import { SafeExpressionEvaluator } from '@memberjunction/global';
11
+ export class DispatcherConditionEvaluator {
12
+ constructor() {
13
+ this.evaluator = new SafeExpressionEvaluator();
14
+ }
15
+ Evaluate(expression, context) {
16
+ try {
17
+ const result = this.evaluator.evaluate(expression, context);
18
+ return result.success
19
+ ? { Success: true, Value: result.value }
20
+ : { Success: false, ErrorMessage: String(result.error ?? 'condition evaluation failed') };
21
+ }
22
+ catch (e) {
23
+ return { Success: false, ErrorMessage: e instanceof Error ? e.message : String(e) };
24
+ }
25
+ }
26
+ }
27
+ //# sourceMappingURL=DispatcherConditionEvaluator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DispatcherConditionEvaluator.js","sourceRoot":"","sources":["../src/DispatcherConditionEvaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,OAAO,4BAA4B;IAAzC;QACqB,cAAS,GAAG,IAAI,uBAAuB,EAAE,CAAC;IAY/D,CAAC;IAVU,QAAQ,CAAC,UAAkB,EAAE,OAAgC;QAChE,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC5D,OAAO,MAAM,CAAC,OAAO;gBACjB,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE;gBACxC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,6BAA6B,CAAC,EAAE,CAAC;QAClG,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,CAAC;IACL,CAAC;CACJ"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @fileoverview The compare-and-swap claim protocol for durable task execution.
3
+ *
4
+ * This is the mechanism that lets more than one dispatcher instance work the same task table
5
+ * without two of them running the same task, and that lets a crashed instance's work be picked up
6
+ * rather than stranded. It is deliberately a small, self-contained unit: every state transition is
7
+ * a guarded `UPDATE ... WHERE <expected state>` whose rowcount is the answer, so correctness rests
8
+ * on the database's own atomicity rather than on a distributed lock manager.
9
+ *
10
+ * **Why rowcount and not read-then-write.** Reading a task, deciding it is claimable, then writing
11
+ * the claim is a textbook race: two instances can both read `Pending`. The single-statement form —
12
+ * `UPDATE Task SET ClaimedBy=@me WHERE ID=@id AND Status='Pending'` — makes the check and the write
13
+ * one atomic operation, so exactly one instance sees rowcount 1 and the other sees 0 and moves on.
14
+ *
15
+ * **Why every transition is guarded, not just the initial claim.** Per D20 the Task table stays
16
+ * user-writable: entity forms, Data Explorer, GraphQL, and any agent holding an update-record action
17
+ * can change `Status` or clear `ClaimedBy` underneath a running executor. A completion write that
18
+ * only said "set this task Complete" would happily overwrite a task someone had reassigned. Guarding
19
+ * on `ClaimedBy=@me` means a stale executor's write fails cleanly (rowcount 0) instead of
20
+ * double-completing, and the dispatcher can defer to the sweep.
21
+ *
22
+ * @module @memberjunction/task-graph
23
+ */
24
+ import { IMetadataProvider, UserInfo } from '@memberjunction/core';
25
+ import { ReconciliationEvent } from './types.js';
26
+ /** Fields the claim protocol needs from a candidate task. */
27
+ export type ClaimableTask = {
28
+ ID: string;
29
+ Name: string;
30
+ AgentID: string | null;
31
+ UserID: string | null;
32
+ InputPayload: string | null;
33
+ };
34
+ /**
35
+ * Guarded reads and writes over the `Task` claim columns.
36
+ *
37
+ * Uses direct SQL rather than `BaseEntity.Save()` on purpose, and this is the one place in the
38
+ * program where that is correct: the entire point is a *conditional* write whose rowcount is the
39
+ * return value. `Save()` issues an unconditional update and reports success for a row whose state
40
+ * changed underneath it, which is precisely the race being defended against. Every method here is a
41
+ * single statement; nothing reads-then-writes.
42
+ */
43
+ export declare class TaskClaimStore {
44
+ private readonly instanceID;
45
+ private readonly claimTTLSeconds;
46
+ constructor(instanceID: string, claimTTLSeconds: number);
47
+ private sql;
48
+ /** Schema-qualified `Task` table for the provider's configured core schema. */
49
+ private taskTable;
50
+ /**
51
+ * Attempts to claim one task.
52
+ *
53
+ * The `Status='Pending'` predicate is the whole contract: a task another instance already moved
54
+ * to `In Progress` fails the predicate and yields rowcount 0. `ClaimedBy IS NULL OR
55
+ * ClaimExpiresAt < now` additionally lets an expired claim be taken over without a separate
56
+ * reconciliation pass having to run first.
57
+ *
58
+ * @returns true when this instance now owns the task
59
+ */
60
+ TryClaim(provider: IMetadataProvider, taskID: string, contextUser: UserInfo): Promise<boolean>;
61
+ /**
62
+ * Extends this instance's claim on a task it is actively running.
63
+ *
64
+ * Guarded on `ClaimedBy=@me` so a heartbeat can never resurrect a claim that reconciliation
65
+ * already released — if the sweep took the task back, the heartbeat fails and the executor
66
+ * learns its work is no longer owned.
67
+ *
68
+ * @returns true when the claim was extended; false means this instance no longer owns the task
69
+ */
70
+ Heartbeat(provider: IMetadataProvider, taskID: string, contextUser: UserInfo): Promise<boolean>;
71
+ /**
72
+ * Records a terminal outcome and releases the claim in one guarded statement.
73
+ *
74
+ * Guarded on both `Status='In Progress'` and `ClaimedBy=@me`: a task that was cancelled or
75
+ * reassigned while running fails the predicate, so a stale executor cannot overwrite the newer
76
+ * decision. The caller treats rowcount 0 as "someone else owns this now" rather than an error.
77
+ *
78
+ * @returns true when this instance's outcome was recorded
79
+ */
80
+ CompleteClaimed(provider: IMetadataProvider, taskID: string, outcome: {
81
+ Status: 'Complete' | 'Failed';
82
+ OutputPayload?: string | null;
83
+ ErrorMessage?: string | null;
84
+ AgentRunID?: string | null;
85
+ }, contextUser: UserInfo): Promise<boolean>;
86
+ /**
87
+ * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
88
+ * them up.
89
+ *
90
+ * **Human tasks are exempt** (review round 2). A task assigned to a person (`UserID` set) never
91
+ * carries a claim, so `In Progress` with no claim is its *legitimate* parked shape — an approval
92
+ * waiting on someone. Normalizing it would reset that approval out from under the user. Their
93
+ * lifecycle is driven by `DueAt` notification and escalation, never by claim expiry.
94
+ *
95
+ * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
96
+ * slow-but-healthy task from being executed twice.
97
+ */
98
+ ReleaseExpiredClaims(provider: IMetadataProvider, contextUser: UserInfo): Promise<ReconciliationEvent[]>;
99
+ /**
100
+ * Returns *agent* tasks sitting `In Progress` with no claim at all.
101
+ *
102
+ * This is the anomalous shape D20 anticipates from a human or an agent writing `Status`
103
+ * directly. It is reported rather than silently corrected: the row is evidence of tampering or
104
+ * of a bug, and Record Changes already carries the audit trail. Human-assigned tasks are
105
+ * excluded because for them this shape is legitimate, not anomalous.
106
+ */
107
+ FindOrphanedInProgress(provider: IMetadataProvider, contextUser: UserInfo): Promise<ReconciliationEvent[]>;
108
+ /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
109
+ private affectedRows;
110
+ private literalOrNull;
111
+ /** Single-quote escaping. Inputs here are UUIDs and JSON the dispatcher itself produced. */
112
+ private escape;
113
+ }
114
+ //# sourceMappingURL=TaskClaimStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskClaimStore.d.ts","sourceRoot":"","sources":["../src/TaskClaimStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,iBAAiB,EAA6C,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC9G,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF;;;;;;;;GAQG;AACH,qBAAa,cAAc;IAEnB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe;gBADf,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM;IAG5C,OAAO,CAAC,GAAG;IAIX,+EAA+E;IAC/E,OAAO,CAAC,SAAS;IAKjB;;;;;;;;;OASG;IACU,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB3G;;;;;;;;OAQG;IACU,SAAS,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAY5G;;;;;;;;OAQG;IACU,eAAe,CACxB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EACnI,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAwBnB;;;;;;;;;;;OAWG;IACU,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IA2CrH;;;;;;;OAOG;IACU,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAqBvH,iGAAiG;YACnF,YAAY;IAe1B,OAAO,CAAC,aAAa;IAIrB,4FAA4F;IAC5F,OAAO,CAAC,MAAM;CAGjB"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @fileoverview The compare-and-swap claim protocol for durable task execution.
3
+ *
4
+ * This is the mechanism that lets more than one dispatcher instance work the same task table
5
+ * without two of them running the same task, and that lets a crashed instance's work be picked up
6
+ * rather than stranded. It is deliberately a small, self-contained unit: every state transition is
7
+ * a guarded `UPDATE ... WHERE <expected state>` whose rowcount is the answer, so correctness rests
8
+ * on the database's own atomicity rather than on a distributed lock manager.
9
+ *
10
+ * **Why rowcount and not read-then-write.** Reading a task, deciding it is claimable, then writing
11
+ * the claim is a textbook race: two instances can both read `Pending`. The single-statement form —
12
+ * `UPDATE Task SET ClaimedBy=@me WHERE ID=@id AND Status='Pending'` — makes the check and the write
13
+ * one atomic operation, so exactly one instance sees rowcount 1 and the other sees 0 and moves on.
14
+ *
15
+ * **Why every transition is guarded, not just the initial claim.** Per D20 the Task table stays
16
+ * user-writable: entity forms, Data Explorer, GraphQL, and any agent holding an update-record action
17
+ * can change `Status` or clear `ClaimedBy` underneath a running executor. A completion write that
18
+ * only said "set this task Complete" would happily overwrite a task someone had reassigned. Guarding
19
+ * on `ClaimedBy=@me` means a stale executor's write fails cleanly (rowcount 0) instead of
20
+ * double-completing, and the dispatcher can defer to the sweep.
21
+ *
22
+ * @module @memberjunction/task-graph
23
+ */
24
+ import { LogError, LogStatus } from '@memberjunction/core';
25
+ /**
26
+ * Guarded reads and writes over the `Task` claim columns.
27
+ *
28
+ * Uses direct SQL rather than `BaseEntity.Save()` on purpose, and this is the one place in the
29
+ * program where that is correct: the entire point is a *conditional* write whose rowcount is the
30
+ * return value. `Save()` issues an unconditional update and reports success for a row whose state
31
+ * changed underneath it, which is precisely the race being defended against. Every method here is a
32
+ * single statement; nothing reads-then-writes.
33
+ */
34
+ export class TaskClaimStore {
35
+ constructor(instanceID, claimTTLSeconds) {
36
+ this.instanceID = instanceID;
37
+ this.claimTTLSeconds = claimTTLSeconds;
38
+ }
39
+ sql(provider) {
40
+ return provider;
41
+ }
42
+ /** Schema-qualified `Task` table for the provider's configured core schema. */
43
+ taskTable(provider) {
44
+ const db = this.sql(provider);
45
+ return `${db.QuoteIdentifier(db.MJCoreSchemaName)}.${db.QuoteIdentifier('Task')}`;
46
+ }
47
+ /**
48
+ * Attempts to claim one task.
49
+ *
50
+ * The `Status='Pending'` predicate is the whole contract: a task another instance already moved
51
+ * to `In Progress` fails the predicate and yields rowcount 0. `ClaimedBy IS NULL OR
52
+ * ClaimExpiresAt < now` additionally lets an expired claim be taken over without a separate
53
+ * reconciliation pass having to run first.
54
+ *
55
+ * @returns true when this instance now owns the task
56
+ */
57
+ async TryClaim(provider, taskID, contextUser) {
58
+ const db = this.sql(provider);
59
+ const expires = new Date(Date.now() + this.claimTTLSeconds * 1000);
60
+ const sql = `
61
+ UPDATE ${this.taskTable(provider)}
62
+ SET ${db.QuoteIdentifier('Status')} = 'In Progress',
63
+ ${db.QuoteIdentifier('ClaimedBy')} = '${this.escape(this.instanceID)}',
64
+ ${db.QuoteIdentifier('ClaimExpiresAt')} = '${expires.toISOString()}',
65
+ ${db.QuoteIdentifier('StartedAt')} = '${new Date().toISOString()}'
66
+ WHERE ${db.QuoteIdentifier('ID')} = '${this.escape(taskID)}'
67
+ AND ${db.QuoteIdentifier('Status')} = 'Pending'
68
+ AND (${db.QuoteIdentifier('ClaimedBy')} IS NULL
69
+ OR ${db.QuoteIdentifier('ClaimExpiresAt')} IS NULL
70
+ OR ${db.QuoteIdentifier('ClaimExpiresAt')} < '${new Date().toISOString()}')`;
71
+ return (await this.affectedRows(db, sql, contextUser)) === 1;
72
+ }
73
+ /**
74
+ * Extends this instance's claim on a task it is actively running.
75
+ *
76
+ * Guarded on `ClaimedBy=@me` so a heartbeat can never resurrect a claim that reconciliation
77
+ * already released — if the sweep took the task back, the heartbeat fails and the executor
78
+ * learns its work is no longer owned.
79
+ *
80
+ * @returns true when the claim was extended; false means this instance no longer owns the task
81
+ */
82
+ async Heartbeat(provider, taskID, contextUser) {
83
+ const db = this.sql(provider);
84
+ const expires = new Date(Date.now() + this.claimTTLSeconds * 1000);
85
+ const sql = `
86
+ UPDATE ${this.taskTable(provider)}
87
+ SET ${db.QuoteIdentifier('ClaimExpiresAt')} = '${expires.toISOString()}'
88
+ WHERE ${db.QuoteIdentifier('ID')} = '${this.escape(taskID)}'
89
+ AND ${db.QuoteIdentifier('ClaimedBy')} = '${this.escape(this.instanceID)}'
90
+ AND ${db.QuoteIdentifier('Status')} = 'In Progress'`;
91
+ return (await this.affectedRows(db, sql, contextUser)) === 1;
92
+ }
93
+ /**
94
+ * Records a terminal outcome and releases the claim in one guarded statement.
95
+ *
96
+ * Guarded on both `Status='In Progress'` and `ClaimedBy=@me`: a task that was cancelled or
97
+ * reassigned while running fails the predicate, so a stale executor cannot overwrite the newer
98
+ * decision. The caller treats rowcount 0 as "someone else owns this now" rather than an error.
99
+ *
100
+ * @returns true when this instance's outcome was recorded
101
+ */
102
+ async CompleteClaimed(provider, taskID, outcome, contextUser) {
103
+ const db = this.sql(provider);
104
+ const sets = [
105
+ `${db.QuoteIdentifier('Status')} = '${outcome.Status}'`,
106
+ `${db.QuoteIdentifier('CompletedAt')} = '${new Date().toISOString()}'`,
107
+ `${db.QuoteIdentifier('PercentComplete')} = ${outcome.Status === 'Complete' ? 100 : 0}`,
108
+ // Release the claim as part of the same atomic write — a separate release could be
109
+ // interrupted, leaving a terminal task holding a claim that the sweep would then flag.
110
+ `${db.QuoteIdentifier('ClaimedBy')} = NULL`,
111
+ `${db.QuoteIdentifier('ClaimExpiresAt')} = NULL`,
112
+ ];
113
+ sets.push(`${db.QuoteIdentifier('OutputPayload')} = ${this.literalOrNull(outcome.OutputPayload)}`);
114
+ sets.push(`${db.QuoteIdentifier('ErrorMessage')} = ${this.literalOrNull(outcome.ErrorMessage)}`);
115
+ sets.push(`${db.QuoteIdentifier('AgentRunID')} = ${outcome.AgentRunID ? `'${this.escape(outcome.AgentRunID)}'` : 'NULL'}`);
116
+ const sql = `
117
+ UPDATE ${this.taskTable(provider)}
118
+ SET ${sets.join(', ')}
119
+ WHERE ${db.QuoteIdentifier('ID')} = '${this.escape(taskID)}'
120
+ AND ${db.QuoteIdentifier('Status')} = 'In Progress'
121
+ AND ${db.QuoteIdentifier('ClaimedBy')} = '${this.escape(this.instanceID)}'`;
122
+ return (await this.affectedRows(db, sql, contextUser)) === 1;
123
+ }
124
+ /**
125
+ * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
126
+ * them up.
127
+ *
128
+ * **Human tasks are exempt** (review round 2). A task assigned to a person (`UserID` set) never
129
+ * carries a claim, so `In Progress` with no claim is its *legitimate* parked shape — an approval
130
+ * waiting on someone. Normalizing it would reset that approval out from under the user. Their
131
+ * lifecycle is driven by `DueAt` notification and escalation, never by claim expiry.
132
+ *
133
+ * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
134
+ * slow-but-healthy task from being executed twice.
135
+ */
136
+ async ReleaseExpiredClaims(provider, contextUser) {
137
+ const db = this.sql(provider);
138
+ const now = new Date().toISOString();
139
+ // Capture what will be reclaimed BEFORE reclaiming, so the log names the tasks. The
140
+ // subsequent UPDATE re-states the same predicate, so a task whose claim was refreshed in
141
+ // between is correctly skipped rather than reclaimed on stale information.
142
+ const candidates = await db.ExecuteSQL(`SELECT ${db.QuoteIdentifier('ID')}, ${db.QuoteIdentifier('Name')}, ${db.QuoteIdentifier('ClaimedBy')}
143
+ FROM ${this.taskTable(provider)}
144
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
145
+ AND ${db.QuoteIdentifier('AgentID')} IS NOT NULL
146
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NOT NULL
147
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} IS NOT NULL
148
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} < '${now}'`, undefined, undefined, contextUser);
149
+ if (!candidates || candidates.length === 0)
150
+ return [];
151
+ const sql = `
152
+ UPDATE ${this.taskTable(provider)}
153
+ SET ${db.QuoteIdentifier('Status')} = 'Pending',
154
+ ${db.QuoteIdentifier('ClaimedBy')} = NULL,
155
+ ${db.QuoteIdentifier('ClaimExpiresAt')} = NULL
156
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
157
+ AND ${db.QuoteIdentifier('AgentID')} IS NOT NULL
158
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NOT NULL
159
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} IS NOT NULL
160
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} < '${now}'`;
161
+ const released = await this.affectedRows(db, sql, contextUser);
162
+ const events = candidates.slice(0, released).map((c) => ({
163
+ TaskID: c.ID,
164
+ Action: 'ExpiredClaimReleased',
165
+ Detail: `Claim held by '${c.ClaimedBy}' expired; task '${c.Name}' returned to Pending.`,
166
+ }));
167
+ for (const e of events) {
168
+ LogStatus(`[TaskGraph reconciliation] ${e.Action}: ${e.Detail}`);
169
+ }
170
+ return events;
171
+ }
172
+ /**
173
+ * Returns *agent* tasks sitting `In Progress` with no claim at all.
174
+ *
175
+ * This is the anomalous shape D20 anticipates from a human or an agent writing `Status`
176
+ * directly. It is reported rather than silently corrected: the row is evidence of tampering or
177
+ * of a bug, and Record Changes already carries the audit trail. Human-assigned tasks are
178
+ * excluded because for them this shape is legitimate, not anomalous.
179
+ */
180
+ async FindOrphanedInProgress(provider, contextUser) {
181
+ const db = this.sql(provider);
182
+ const rows = await db.ExecuteSQL(`SELECT ${db.QuoteIdentifier('ID')}, ${db.QuoteIdentifier('Name')}
183
+ FROM ${this.taskTable(provider)}
184
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
185
+ AND ${db.QuoteIdentifier('AgentID')} IS NOT NULL
186
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NULL`, undefined, undefined, contextUser);
187
+ const events = (rows ?? []).map((r) => ({
188
+ TaskID: r.ID,
189
+ Action: 'OrphanedInProgressReleased',
190
+ Detail: `Agent task '${r.Name}' is In Progress with no claim — no dispatcher owns it.`,
191
+ }));
192
+ for (const e of events) {
193
+ LogError(`[TaskGraph reconciliation] ${e.Action}: ${e.Detail}`);
194
+ }
195
+ return events;
196
+ }
197
+ /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
198
+ async affectedRows(db, sql, contextUser) {
199
+ try {
200
+ // Trailing SELECT is how the row count comes back as data across both dialects, rather
201
+ // than depending on a driver-specific rowsAffected field.
202
+ const rows = await db.ExecuteSQL(`${sql};\nSELECT @@ROWCOUNT AS ${db.QuoteIdentifier('AffectedRows')}`, undefined, undefined, contextUser);
203
+ return Number(rows?.[0]?.AffectedRows ?? 0);
204
+ }
205
+ catch (e) {
206
+ LogError(`[TaskGraph] guarded write failed: ${e instanceof Error ? e.message : String(e)}`);
207
+ return 0;
208
+ }
209
+ }
210
+ literalOrNull(value) {
211
+ return value == null ? 'NULL' : `'${this.escape(value)}'`;
212
+ }
213
+ /** Single-quote escaping. Inputs here are UUIDs and JSON the dispatcher itself produced. */
214
+ escape(value) {
215
+ return value.replace(/'/g, "''");
216
+ }
217
+ }
218
+ //# sourceMappingURL=TaskClaimStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskClaimStore.js","sourceRoot":"","sources":["../src/TaskClaimStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
1
+ import { UserInfo } from '@memberjunction/core';
2
+ import { IShutdownable } from '@memberjunction/global';
3
+ import { ProviderFactory, TaskAgentRunner, TaskGraphDispatcherConfig, type TaskContinuationDeliverer, type TaskGraphObserver } from './types.js';
4
+ export declare class TaskGraphDispatcher implements IShutdownable {
5
+ private readonly providerFactory;
6
+ private readonly agentRunner;
7
+ private readonly contextUser;
8
+ /**
9
+ * Optional. Absent means a host that cannot post messages or start agent turns — a worker,
10
+ * a test. The dispatcher still records and logs every completion, so a graph's outcome is
11
+ * never lost; it simply is not announced.
12
+ */
13
+ private readonly continuationDeliverer?;
14
+ /**
15
+ * Optional. Absent means nobody is watching — the dispatcher behaves identically, it just
16
+ * announces nothing.
17
+ */
18
+ private readonly observer?;
19
+ private readonly config;
20
+ private readonly claims;
21
+ private readonly conditionEvaluator;
22
+ private running;
23
+ private pollTimer;
24
+ private reconcileTimer;
25
+ /** Tasks this instance is currently executing — bounds concurrency and drives heartbeats. */
26
+ private readonly inFlight;
27
+ /** Guards against a slow poll overlapping the next tick. */
28
+ private polling;
29
+ /** Graph → owning user, from the parent's durable metadata. Ownership never changes, so this never goes stale. */
30
+ private readonly ownerByParentID;
31
+ constructor(providerFactory: ProviderFactory, agentRunner: TaskAgentRunner, contextUser: UserInfo, config: Partial<TaskGraphDispatcherConfig> & Pick<TaskGraphDispatcherConfig, 'InstanceID'>,
32
+ /**
33
+ * Optional. Absent means a host that cannot post messages or start agent turns — a worker,
34
+ * a test. The dispatcher still records and logs every completion, so a graph's outcome is
35
+ * never lost; it simply is not announced.
36
+ */
37
+ continuationDeliverer?: TaskContinuationDeliverer,
38
+ /**
39
+ * Optional. Absent means nobody is watching — the dispatcher behaves identically, it just
40
+ * announces nothing.
41
+ */
42
+ observer?: TaskGraphObserver);
43
+ /**
44
+ * Announce something that happened, and never let the announcement matter.
45
+ *
46
+ * A frame is commentary on work, never a step of it, so an observer that throws must not be able
47
+ * to fail a task or stall a graph. Swallowing here rather than asking every implementation to be
48
+ * careful means one place enforces it.
49
+ */
50
+ private emit;
51
+ /**
52
+ * Who a graph belongs to, memoized for the process's lifetime.
53
+ *
54
+ * Read from the parent's durable metadata rather than a column, because `Task.UserID` means
55
+ * "the person this task is waiting on" — setting it on a parent would make every graph look
56
+ * like a human task. Memoized because frames are emitted per step: without the cache, watching
57
+ * a run would cost one query per event, and observability that scales with work is the thing a
58
+ * push mechanism exists to avoid. Ownership never changes for a given graph, so the cache can
59
+ * never go stale.
60
+ *
61
+ * Skipped entirely when nobody is observing — the lookup exists only to address frames.
62
+ */
63
+ private resolveOwner;
64
+ /**
65
+ * Begins dispatching.
66
+ *
67
+ * Runs reconciliation FIRST, before accepting any new work. On a restart this instance may be
68
+ * looking at tasks its own previous incarnation claimed and never released — reclaiming those
69
+ * up front is what turns a crash from "work stranded forever" into "work resumes".
70
+ */
71
+ Start(): Promise<void>;
72
+ /**
73
+ * Stops accepting new work and waits for in-flight tasks to finish.
74
+ *
75
+ * Deliberately does NOT release claims on the way out: an abandoned claim expires on its own,
76
+ * and releasing eagerly would hand a still-running task to another instance mid-execution.
77
+ * Letting the TTL do it is the safer failure mode.
78
+ */
79
+ Stop(): Promise<void>;
80
+ /** Name shown in the shutdown drain log. */
81
+ readonly ShutdownName = "TaskGraphDispatcher";
82
+ /** {@link IShutdownable} — idempotent by way of `Stop`'s `running` guard. */
83
+ Shutdown(): Promise<void>;
84
+ /**
85
+ * Reclaims expired claims and reports anomalies.
86
+ *
87
+ * Also enforces the two schema promises that previously had no enforcer anywhere: agent tasks
88
+ * left `In Progress` with no claim are surfaced loudly rather than silently corrected, since
89
+ * that shape indicates tampering or a bug and Record Changes already carries the audit trail.
90
+ */
91
+ Reconcile(): Promise<void>;
92
+ /**
93
+ * One dispatch pass: find claimable work, claim what fits under the concurrency cap, execute.
94
+ *
95
+ * Overlap-guarded — a pass that runs long simply skips the next tick rather than stacking, which
96
+ * would otherwise let a slow database multiply in-flight work past the cap.
97
+ */
98
+ private pollOnce;
99
+ /**
100
+ * Executes one claimed task on its own provider, heartbeating until it settles.
101
+ *
102
+ * A fresh provider per task is the point of `ProviderFactory`: parallel tasks must not share a
103
+ * transaction scope or entity instances, or one task's work becomes visible inside another's.
104
+ */
105
+ private executeClaimed;
106
+ /**
107
+ * Applies failure propagation and parent rollup across every graph with active work.
108
+ *
109
+ * All four decisions — what is eligible, what must block, what the parent status is, whether the
110
+ * graph is wedged — are delegated to the pure algorithms, unchanged from Phase 1.
111
+ */
112
+ private propagateAndRollup;
113
+ /**
114
+ * Runs the graph's continuation exactly once, now that it has settled.
115
+ *
116
+ * **Why the delivery marker is written before the side effect.** Delivery is at-least-once by
117
+ * nature: the process can die between "the graph is done" and "the user has been told". Marking
118
+ * first and acting second means the worst case is a *missed* notification that shows up in the
119
+ * task record as delivered — recoverable, visible, and inspectable. Marking after would make the
120
+ * worst case a *repeated* notification on every reconciliation sweep, forever, which is both
121
+ * user-visible noise and, for `reinvoke`, an unbounded agent-run loop. Given one of the two has
122
+ * to be chosen, the quiet failure is the safe one.
123
+ *
124
+ * The marker is written with a compare-and-swap read-back, so two instances reconciling the same
125
+ * completed graph produce one winner rather than two.
126
+ */
127
+ private deliverContinuation;
128
+ /** Reads the parent's durable continuation metadata through the shared parser. */
129
+ private readParentMetadata;
130
+ /**
131
+ * Stamps the delivery marker and confirms this instance won the race.
132
+ *
133
+ * `MJ: Tasks` stays user-writable (D20), so a plain "read, decide, write" is not enough — the
134
+ * read-back is what makes a lost race observable instead of producing a duplicate delivery.
135
+ */
136
+ private claimContinuation;
137
+ /** One line describing how the graph ended, for the completion log and message delivery. */
138
+ private buildContinuationSummary;
139
+ /**
140
+ * Tells the assignee that a human task is ready, exactly once.
141
+ *
142
+ * **Once** matters more than it looks: eligibility is recomputed on every poll, so a task parked
143
+ * on a person for three days would otherwise re-notify every five seconds until they acted. The
144
+ * marker is the task's own `ClaimedBy` — a human task has no executor to claim it, so the column
145
+ * is free, and reusing it means the "already notified" fact is as durable and as crash-safe as
146
+ * every other piece of graph state. A restart cannot resend.
147
+ *
148
+ * Best-effort by design. A notification that fails to send must not stop the graph or the poll
149
+ * loop; the task is still visible in the Tasks UI, so the work is discoverable even when the
150
+ * nudge does not arrive.
151
+ */
152
+ private notifyHumanTaskReady;
153
+ /**
154
+ * Parent tasks that still have work to do.
155
+ *
156
+ * `BypassCache` for the reason the caching guide names explicitly: **the claim protocol mutates
157
+ * these rows through direct SQL**, because the CAS guarantee IS the database's atomicity and a
158
+ * `BaseEntity.Save()` cannot express a guarded UPDATE. Direct DML fires no invalidation event,
159
+ * so a cached read of this query is stale the instant any task is claimed or completed — and
160
+ * the dispatcher would then be reading its own work queue through a cache its own writes never
161
+ * invalidate. Left cached, a completed task keeps reading as `In Progress` and the graph never
162
+ * rolls up: submitted work simply never settles.
163
+ */
164
+ private findActiveGraphIDs;
165
+ /**
166
+ * Tasks eligible to claim right now, across all active graphs.
167
+ *
168
+ * Eligibility is decided by the pure algorithm rather than by SQL: expressing "all prerequisites
169
+ * complete" as a query is possible but would be a second, independently-maintained definition of
170
+ * the same rule, free to drift from the one the in-run executor uses.
171
+ */
172
+ private findClaimableTasks;
173
+ /** Loads a graph's children and edges in the shapes both the algorithms and mutation need. */
174
+ private loadGraphState;
175
+ /**
176
+ * Decides whether a conditional dependency edge is live.
177
+ *
178
+ * The condition sees the upstream task's outcome — its status and parsed output — which is the
179
+ * only information a runtime graph has to branch on. Returns `'drop'` only on a definite false;
180
+ * an unevaluable condition keeps the edge for the reason stated at the call site.
181
+ */
182
+ private evaluateEdgeCondition;
183
+ /** Parsed `OutputPayload` of each completed dependency, keyed by that task's ID. */
184
+ private loadDependencyOutputs;
185
+ }
186
+ //# sourceMappingURL=TaskGraphDispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskGraphDispatcher.d.ts","sourceRoot":"","sources":["../src/TaskGraphDispatcher.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAmD,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EAAE,aAAa,EAAoB,MAAM,wBAAwB,CAAC;AAkBzE,OAAO,EAEH,eAAe,EACf,eAAe,EACf,yBAAyB,EACzB,KAAK,yBAAyB,EAG9B,KAAK,iBAAiB,EACzB,MAAM,SAAS,CAAC;AAcjB,qBAAa,mBAAoB,YAAW,aAAa;IAiBjD,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAE5B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC;IACvC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IA9B9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAElE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,cAAc,CAA+C;IACrE,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,4DAA4D;IAC5D,OAAO,CAAC,OAAO,CAAS;IAExB,kHAAkH;IAClH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;gBAG/C,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,eAAe,EAC5B,WAAW,EAAE,QAAQ,EACtC,MAAM,EAAE,OAAO,CAAC,yBAAyB,CAAC,GAAG,IAAI,CAAC,yBAAyB,EAAE,YAAY,CAAC;IAC1F;;;;OAIG;IACc,qBAAqB,CAAC,EAAE,yBAAyB;IAClE;;;OAGG;IACc,QAAQ,CAAC,EAAE,iBAAiB;IAOjD;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;IASZ;;;;;;;;;;;OAWG;YACW,YAAY;IAmB1B;;;;;;OAMG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnC;;;;;;OAMG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,4CAA4C;IAC5C,SAAgB,YAAY,yBAAyB;IAErD,6EAA6E;IAChE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;;;;;OAMG;IACU,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBvC;;;;;OAKG;YACW,QAAQ;IA+BtB;;;;;OAKG;YACW,cAAc;IAsF5B;;;;;OAKG;YACW,kBAAkB;IA8DhC;;;;;;;;;;;;;OAaG;YACW,mBAAmB;IAgEjC,kFAAkF;IAClF,OAAO,CAAC,kBAAkB;IAI1B;;;;;OAKG;YACW,iBAAiB;IAmB/B,4FAA4F;IAC5F,OAAO,CAAC,wBAAwB;IAOhC;;;;;;;;;;;;OAYG;YACW,oBAAoB;IAsClC;;;;;;;;;;OAUG;YACW,kBAAkB;IA2ChC;;;;;;OAMG;YACW,kBAAkB;IA4BhC,8FAA8F;YAChF,cAAc;IA8D5B;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAgC7B,oFAAoF;YACtE,qBAAqB;CAkBtC"}