@memberjunction/task-graph 0.0.0 → 6.1.0-edge.2

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 (51) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +167 -27
  3. package/dist/DispatcherConditionEvaluator.d.ts +10 -0
  4. package/dist/DispatcherConditionEvaluator.d.ts.map +1 -0
  5. package/dist/DispatcherConditionEvaluator.js +27 -0
  6. package/dist/DispatcherConditionEvaluator.js.map +1 -0
  7. package/dist/TaskClaimStore.d.ts +125 -0
  8. package/dist/TaskClaimStore.d.ts.map +1 -0
  9. package/dist/TaskClaimStore.js +223 -0
  10. package/dist/TaskClaimStore.js.map +1 -0
  11. package/dist/TaskGraphDispatcher.d.ts +548 -0
  12. package/dist/TaskGraphDispatcher.d.ts.map +1 -0
  13. package/dist/TaskGraphDispatcher.js +2232 -0
  14. package/dist/TaskGraphDispatcher.js.map +1 -0
  15. package/dist/TaskGraphService.d.ts +247 -0
  16. package/dist/TaskGraphService.d.ts.map +1 -0
  17. package/dist/TaskGraphService.js +753 -0
  18. package/dist/TaskGraphService.js.map +1 -0
  19. package/dist/TaskGraphSubmitterImpl.d.ts +7 -0
  20. package/dist/TaskGraphSubmitterImpl.d.ts.map +1 -0
  21. package/dist/TaskGraphSubmitterImpl.js +52 -0
  22. package/dist/TaskGraphSubmitterImpl.js.map +1 -0
  23. package/dist/TaskLoopExecutor.d.ts +62 -0
  24. package/dist/TaskLoopExecutor.d.ts.map +1 -0
  25. package/dist/TaskLoopExecutor.js +248 -0
  26. package/dist/TaskLoopExecutor.js.map +1 -0
  27. package/dist/WorkflowSpecSync.d.ts +197 -0
  28. package/dist/WorkflowSpecSync.d.ts.map +1 -0
  29. package/dist/WorkflowSpecSync.js +474 -0
  30. package/dist/WorkflowSpecSync.js.map +1 -0
  31. package/dist/index.d.ts +19 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/operations/TaskGraphOperations.d.ts +39 -0
  36. package/dist/operations/TaskGraphOperations.d.ts.map +1 -0
  37. package/dist/operations/TaskGraphOperations.js +168 -0
  38. package/dist/operations/TaskGraphOperations.js.map +1 -0
  39. package/dist/operations/WorkflowDraftOperation.d.ts +37 -0
  40. package/dist/operations/WorkflowDraftOperation.d.ts.map +1 -0
  41. package/dist/operations/WorkflowDraftOperation.js +141 -0
  42. package/dist/operations/WorkflowDraftOperation.js.map +1 -0
  43. package/dist/operations/WorkflowOperations.d.ts +22 -0
  44. package/dist/operations/WorkflowOperations.d.ts.map +1 -0
  45. package/dist/operations/WorkflowOperations.js +99 -0
  46. package/dist/operations/WorkflowOperations.js.map +1 -0
  47. package/dist/types.d.ts +328 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +9 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +36 -8
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.
package/README.md CHANGED
@@ -1,45 +1,185 @@
1
1
  # @memberjunction/task-graph
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Durable execution of task graphs: submission, the dispatcher, the claim protocol, and the runners
4
+ that turn a node into work.
4
5
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
+ > **New to workflows?** Start with the [Workflows and Task Graphs
7
+ > Guide](../../guides/WORKFLOW_AND_TASK_GRAPH_GUIDE.md), which covers what a workflow is, when to
8
+ > use one, and every rule that decides what happens next. This README is the package tour.
6
9
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
10
+ ---
11
+
12
+ ## What this package is for
13
+
14
+ A task graph is work that has to **outlive the thing that asked for it**. Before durable execution,
15
+ a multi-step agent plan lived inside one agent run: a page reload lost it, a server restart orphaned
16
+ it, and no channel other than the one that started it could see it.
17
+
18
+ This package is the other half of that split. Something else *produces* a graph; this package makes
19
+ it durable and runs it.
20
+
21
+ ```mermaid
22
+ graph TB
23
+ subgraph Producers["Producers — anyone"]
24
+ P1["Flow agent<br/><i>compiled</i>"]
25
+ P2["Loop agent<br/><i>emitted</i>"]
26
+ P3["Entity action"]
27
+ P4["TaskGraph.Submit<br/><i>remote op</i>"]
28
+ end
29
+
30
+ P1 & P2 & P3 & P4 --> SVC["<b>TaskGraphService.Submit</b><br/>validate · resolve · persist"]
31
+ SVC --> ROWS["Task + TaskDependency"]
32
+ ROWS --> DISP["<b>TaskGraphDispatcher</b><br/>poll · claim · execute · settle"]
33
+
34
+ DISP --> R1["TaskAgentRunner"]
35
+ DISP --> R2["TaskActionRunner"]
36
+ DISP --> R3["TaskLoopExecutor"]
37
+ DISP --> R4["a person"]
38
+
39
+ style SVC fill:#7c5295,stroke:#563a6b,color:#fff
40
+ style ROWS fill:#2d8659,stroke:#1a5c3a,color:#fff
41
+ style DISP fill:#b8762f,stroke:#8a5722,color:#fff
42
+ ```
43
+
44
+ **Submission never waits for execution.** `Submit` returns as soon as the graph is durable. That
45
+ split is what makes the engine invocation-agnostic: an agent, a scheduled job, a Slack message and a
46
+ manual UI all call the same method, and whichever dispatcher instance is running picks the work up.
47
+
48
+ ---
49
+
50
+ ## What it deliberately does not do
51
+
52
+ **It does not decide graph semantics.** Eligibility, failure propagation, parent rollup, skip
53
+ cascade, exclusive-group resolution and stall detection all come from pure, dependency-free
54
+ functions in [`@memberjunction/ai-core-plus`](../AI/CorePlus). That is not tidiness — it is what
55
+ stops the in-run executor and the durable executor from drifting apart. Neither owns the rules.
56
+
57
+ **It does not import MJServer.** Provider minting and agent execution arrive as injected seams
58
+ (`ProviderFactory`, `TaskAgentRunner`, `TaskActionRunner`), so the dependency runs
59
+ MJServer → task-graph and never the reverse. That is also what keeps the dispatcher unit-testable
60
+ without standing up the agent framework.
61
+
62
+ ---
63
+
64
+ ## The pieces
65
+
66
+ | File | Responsibility |
67
+ |---|---|
68
+ | `TaskGraphService.ts` | Validate a spec, resolve names to IDs, write parent + children + edges. The only write path. |
69
+ | `TaskGraphDispatcher.ts` | Poll, claim, execute, evaluate edges, propagate, roll up, settle, credit cost. |
70
+ | `TaskClaimStore.ts` | The atomic claim protocol — guarded writes so two instances cannot run the same task. |
71
+ | `TaskLoopExecutor.ts` | `ForEach` / `While` semantics: bounds, ordering, concurrency, delay, failure. |
72
+ | `DispatcherConditionEvaluator.ts` | Edge conditions, over the superset context both dialects can read. |
73
+ | `TaskGraphSubmitterImpl.ts` | Registers the durable submitter under the `ClassFactory` seam. |
74
+ | `operations/` | The `TaskGraph.*` remote operations. |
8
75
 
9
- ## Purpose
76
+ ---
77
+
78
+ ## Claiming: why a task runs exactly once
79
+
80
+ Multiple dispatcher instances poll the same table. Correctness comes from a **guarded write**, not
81
+ from coordination: claiming is an update that only succeeds if the row still looks the way the
82
+ claimer expects.
83
+
84
+ ```mermaid
85
+ sequenceDiagram
86
+ participant A as Instance A
87
+ participant B as Instance B
88
+ participant DB as Task row
89
+
90
+ A->>DB: claim if unclaimed / expired
91
+ B->>DB: claim if unclaimed / expired
92
+ DB-->>A: 1 row updated ✅
93
+ DB-->>B: 0 rows updated ❌
94
+ Note over B: defers — no lock, no retry storm
95
+ loop while running
96
+ A->>DB: heartbeat (extends ClaimExpiresAt)
97
+ end
98
+ A->>DB: complete IF still owned by A
99
+ ```
100
+
101
+ If A dies, its claim expires and reconciliation makes the task claimable again. If A finishes but
102
+ the row changed underneath it (cancelled, reassigned, reclaimed), the guarded completion refuses and
103
+ A defers — overwriting would undo a newer, deliberate decision.
104
+
105
+ ---
106
+
107
+ ## Runners are seams, not implementations
108
+
109
+ | Seam | Implemented by | Absent means |
110
+ |---|---|---|
111
+ | `TaskAgentRunner` | `TaskGraphAgentRunner` (MJServer) | agent nodes cannot run here |
112
+ | `TaskActionRunner` | `TaskGraphActionRunner` (MJServer) | action nodes stay `Pending`, **not** Failed |
113
+ | `ProviderFactory` | `TaskGraphProviderFactory` (MJServer) | required |
114
+ | `TaskContinuationDeliverer` | `TaskGraphContinuationDeliverer` (MJServer) | outcomes are recorded but not announced |
115
+ | `TaskGraphObserver` | the frame resolver (MJServer) | nobody is watching; behaviour identical |
116
+
117
+ **A host with no runner is limited, not broken.** "Nobody here can run this" is not the same as
118
+ "this ran and did not work", so those tasks stay visible and claimable by an instance that can.
119
+
120
+ > **An Agent node starts a brand-new `AIAgentRun`** — a root run, linked from `Task.AgentRunID`
121
+ > rather than nested under the submitting run. The relationship is expressed by the Task row.
122
+
123
+ ---
124
+
125
+ ## Starting a dispatcher
10
126
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/task-graph`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
127
+ MJAPI does this for you (`StartTaskGraphDispatcher`). For a custom host:
15
128
 
16
- ## What is OIDC Trusted Publishing?
129
+ ```typescript
130
+ import { TaskGraphDispatcher, LoadTaskGraphOperations } from '@memberjunction/task-graph';
17
131
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
132
+ LoadTaskGraphOperations(); // registers the remote operations + the durable submitter
19
133
 
20
- ## Setup Instructions
134
+ const dispatcher = new TaskGraphDispatcher(
135
+ providerFactory,
136
+ agentRunner,
137
+ contextUser,
138
+ { InstanceID: `worker-${process.pid}` },
139
+ continuationDeliverer, // optional
140
+ observer, // optional
141
+ actionRunner, // optional
142
+ );
143
+ await dispatcher.Start();
144
+ ```
21
145
 
22
- To properly configure OIDC trusted publishing for this package:
146
+ Without `LoadTaskGraphOperations()` nothing registers the submitter, and
147
+ `GetTaskGraphSubmitter()` returns `null` — which callers must report rather than swallow.
23
148
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
149
+ ---
28
150
 
29
- ## DO NOT USE THIS PACKAGE
151
+ ## Cost rollup
30
152
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
153
+ When a graph settles, the dispatcher credits its spending back to the run that submitted it, writing
154
+ the `…Rollup` columns on `AIAgentRun`:
36
155
 
37
- ## More Information
156
+ - `TotalCost` — what the submitting run itself spent. **Never rewritten here.**
157
+ - `TotalCostRollup` — that run plus everything it caused.
38
158
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
159
+ This cannot happen during the run: a submitting run *ends at submission*, so at the moment it
160
+ computes its own totals the graph has not spent anything yet. See the guide's [cost
161
+ section](../../guides/WORKFLOW_AND_TASK_GRAPH_GUIDE.md#cost-and-tokens-the-seam).
42
162
 
43
163
  ---
44
164
 
45
- **Maintained for OIDC setup purposes only**
165
+ ## Testing
166
+
167
+ ```bash
168
+ cd packages/TaskGraph && pnpm test
169
+ ```
170
+
171
+ Unit tests cover the pure pieces — loop semantics, dispatchable kinds, configuration persistence,
172
+ parent metadata. The dispatcher driving **real rows against SQL Server** is covered by the `IT74`
173
+ bundle in `@memberjunction/integration-test-suite`, which uses a stub agent runner so it stays in
174
+ the deterministic tier: no model calls, no tokens, real claim protocol, real condition evaluator,
175
+ real rollup.
176
+
177
+ ---
178
+
179
+ ## Related
180
+
181
+ - [Workflows and Task Graphs Guide](../../guides/WORKFLOW_AND_TASK_GRAPH_GUIDE.md) — start here
182
+ - [`@memberjunction/ai-core-plus`](../AI/CorePlus) — the spec, validator, compiler, pure algorithms,
183
+ payload mapping and layout
184
+ - [`@memberjunction/ai-agents`](../AI/Agents) — the agent framework and `FlowAgentType`
185
+ - [`packages/Actions/CLAUDE.md`](../Actions/CLAUDE.md) — Actions as boundaries
@@ -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,125 @@
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
+ /**
86
+ * The step's Configuration bag, when the run produced something that belongs in it.
87
+ *
88
+ * Written in the SAME guarded UPDATE as the rest of the outcome rather than a follow-up
89
+ * save, because a second write could land after the row was reclaimed and would then
90
+ * attribute one instance's runtime artefacts to another instance's execution.
91
+ *
92
+ * Omitted leaves the column untouched — a step whose run produces no artefacts must not
93
+ * have its authored configuration blanked as a side effect of finishing.
94
+ */
95
+ Configuration?: string | null;
96
+ }, contextUser: UserInfo): Promise<boolean>;
97
+ /**
98
+ * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
99
+ * them up.
100
+ *
101
+ * **Human tasks are exempt** (review round 2). A task assigned to a person (`UserID` set) never
102
+ * carries a claim, so `In Progress` with no claim is its *legitimate* parked shape — an approval
103
+ * waiting on someone. Normalizing it would reset that approval out from under the user. Their
104
+ * lifecycle is driven by `DueAt` notification and escalation, never by claim expiry.
105
+ *
106
+ * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
107
+ * slow-but-healthy task from being executed twice.
108
+ */
109
+ ReleaseExpiredClaims(provider: IMetadataProvider, contextUser: UserInfo): Promise<ReconciliationEvent[]>;
110
+ /**
111
+ * Returns *agent* tasks sitting `In Progress` with no claim at all.
112
+ *
113
+ * This is the anomalous shape D20 anticipates from a human or an agent writing `Status`
114
+ * directly. It is reported rather than silently corrected: the row is evidence of tampering or
115
+ * of a bug, and Record Changes already carries the audit trail. Human-assigned tasks are
116
+ * excluded because for them this shape is legitimate, not anomalous.
117
+ */
118
+ FindOrphanedInProgress(provider: IMetadataProvider, contextUser: UserInfo): Promise<ReconciliationEvent[]>;
119
+ /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
120
+ private affectedRows;
121
+ private literalOrNull;
122
+ /** Single-quote escaping. Inputs here are UUIDs and JSON the dispatcher itself produced. */
123
+ private escape;
124
+ }
125
+ //# 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;QACL,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAC;QAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B;;;;;;;;;WASG;QACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,EACD,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IA6BnB;;;;;;;;;;;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,223 @@
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
+ // Only when supplied — see the note on the parameter. `undefined` means "leave it alone",
117
+ // which is not the same as an explicit null.
118
+ if (outcome.Configuration !== undefined) {
119
+ sets.push(`${db.QuoteIdentifier('Configuration')} = ${this.literalOrNull(outcome.Configuration)}`);
120
+ }
121
+ const sql = `
122
+ UPDATE ${this.taskTable(provider)}
123
+ SET ${sets.join(', ')}
124
+ WHERE ${db.QuoteIdentifier('ID')} = '${this.escape(taskID)}'
125
+ AND ${db.QuoteIdentifier('Status')} = 'In Progress'
126
+ AND ${db.QuoteIdentifier('ClaimedBy')} = '${this.escape(this.instanceID)}'`;
127
+ return (await this.affectedRows(db, sql, contextUser)) === 1;
128
+ }
129
+ /**
130
+ * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
131
+ * them up.
132
+ *
133
+ * **Human tasks are exempt** (review round 2). A task assigned to a person (`UserID` set) never
134
+ * carries a claim, so `In Progress` with no claim is its *legitimate* parked shape — an approval
135
+ * waiting on someone. Normalizing it would reset that approval out from under the user. Their
136
+ * lifecycle is driven by `DueAt` notification and escalation, never by claim expiry.
137
+ *
138
+ * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
139
+ * slow-but-healthy task from being executed twice.
140
+ */
141
+ async ReleaseExpiredClaims(provider, contextUser) {
142
+ const db = this.sql(provider);
143
+ const now = new Date().toISOString();
144
+ // Capture what will be reclaimed BEFORE reclaiming, so the log names the tasks. The
145
+ // subsequent UPDATE re-states the same predicate, so a task whose claim was refreshed in
146
+ // between is correctly skipped rather than reclaimed on stale information.
147
+ const candidates = await db.ExecuteSQL(`SELECT ${db.QuoteIdentifier('ID')}, ${db.QuoteIdentifier('Name')}, ${db.QuoteIdentifier('ClaimedBy')}
148
+ FROM ${this.taskTable(provider)}
149
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
150
+ AND (${db.QuoteIdentifier('AgentID')} IS NOT NULL OR ${db.QuoteIdentifier('ActionID')} IS NOT NULL)
151
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NOT NULL
152
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} IS NOT NULL
153
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} < '${now}'`, undefined, undefined, contextUser);
154
+ if (!candidates || candidates.length === 0)
155
+ return [];
156
+ const sql = `
157
+ UPDATE ${this.taskTable(provider)}
158
+ SET ${db.QuoteIdentifier('Status')} = 'Pending',
159
+ ${db.QuoteIdentifier('ClaimedBy')} = NULL,
160
+ ${db.QuoteIdentifier('ClaimExpiresAt')} = NULL
161
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
162
+ AND (${db.QuoteIdentifier('AgentID')} IS NOT NULL OR ${db.QuoteIdentifier('ActionID')} IS NOT NULL)
163
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NOT NULL
164
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} IS NOT NULL
165
+ AND ${db.QuoteIdentifier('ClaimExpiresAt')} < '${now}'`;
166
+ const released = await this.affectedRows(db, sql, contextUser);
167
+ const events = candidates.slice(0, released).map((c) => ({
168
+ TaskID: c.ID,
169
+ Action: 'ExpiredClaimReleased',
170
+ Detail: `Claim held by '${c.ClaimedBy}' expired; task '${c.Name}' returned to Pending.`,
171
+ }));
172
+ for (const e of events) {
173
+ LogStatus(`[TaskGraph reconciliation] ${e.Action}: ${e.Detail}`);
174
+ }
175
+ return events;
176
+ }
177
+ /**
178
+ * Returns *agent* tasks sitting `In Progress` with no claim at all.
179
+ *
180
+ * This is the anomalous shape D20 anticipates from a human or an agent writing `Status`
181
+ * directly. It is reported rather than silently corrected: the row is evidence of tampering or
182
+ * of a bug, and Record Changes already carries the audit trail. Human-assigned tasks are
183
+ * excluded because for them this shape is legitimate, not anomalous.
184
+ */
185
+ async FindOrphanedInProgress(provider, contextUser) {
186
+ const db = this.sql(provider);
187
+ const rows = await db.ExecuteSQL(`SELECT ${db.QuoteIdentifier('ID')}, ${db.QuoteIdentifier('Name')}
188
+ FROM ${this.taskTable(provider)}
189
+ WHERE ${db.QuoteIdentifier('Status')} = 'In Progress'
190
+ AND (${db.QuoteIdentifier('AgentID')} IS NOT NULL OR ${db.QuoteIdentifier('ActionID')} IS NOT NULL)
191
+ AND ${db.QuoteIdentifier('ClaimedBy')} IS NULL`, undefined, undefined, contextUser);
192
+ const events = (rows ?? []).map((r) => ({
193
+ TaskID: r.ID,
194
+ Action: 'OrphanedInProgressReleased',
195
+ Detail: `Agent task '${r.Name}' is In Progress with no claim — no dispatcher owns it.`,
196
+ }));
197
+ for (const e of events) {
198
+ LogError(`[TaskGraph reconciliation] ${e.Action}: ${e.Detail}`);
199
+ }
200
+ return events;
201
+ }
202
+ /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
203
+ async affectedRows(db, sql, contextUser) {
204
+ try {
205
+ // Trailing SELECT is how the row count comes back as data across both dialects, rather
206
+ // than depending on a driver-specific rowsAffected field.
207
+ const rows = await db.ExecuteSQL(`${sql};\nSELECT @@ROWCOUNT AS ${db.QuoteIdentifier('AffectedRows')}`, undefined, undefined, contextUser);
208
+ return Number(rows?.[0]?.AffectedRows ?? 0);
209
+ }
210
+ catch (e) {
211
+ LogError(`[TaskGraph] guarded write failed: ${e instanceof Error ? e.message : String(e)}`);
212
+ return 0;
213
+ }
214
+ }
215
+ literalOrNull(value) {
216
+ return value == null ? 'NULL' : `'${this.escape(value)}'`;
217
+ }
218
+ /** Single-quote escaping. Inputs here are UUIDs and JSON the dispatcher itself produced. */
219
+ escape(value) {
220
+ return value.replace(/'/g, "''");
221
+ }
222
+ }
223
+ //# sourceMappingURL=TaskClaimStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskClaimStore.js","sourceRoot":"","sources":["../src/TaskClaimStore.ts"],"names":[],"mappings":"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"}