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

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 (53) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +30 -1
  3. package/dist/TaskClaimStore.d.ts +376 -4
  4. package/dist/TaskClaimStore.d.ts.map +1 -1
  5. package/dist/TaskClaimStore.js +600 -20
  6. package/dist/TaskClaimStore.js.map +1 -1
  7. package/dist/TaskGraphDispatcher.d.ts +326 -25
  8. package/dist/TaskGraphDispatcher.d.ts.map +1 -1
  9. package/dist/TaskGraphDispatcher.js +1604 -286
  10. package/dist/TaskGraphDispatcher.js.map +1 -1
  11. package/dist/TaskGraphService.d.ts +255 -5
  12. package/dist/TaskGraphService.d.ts.map +1 -1
  13. package/dist/TaskGraphService.js +583 -26
  14. package/dist/TaskGraphService.js.map +1 -1
  15. package/dist/TaskGraphSubmitterImpl.d.ts.map +1 -1
  16. package/dist/TaskGraphSubmitterImpl.js +5 -0
  17. package/dist/TaskGraphSubmitterImpl.js.map +1 -1
  18. package/dist/condition-gate.d.ts +128 -0
  19. package/dist/condition-gate.d.ts.map +1 -0
  20. package/dist/condition-gate.js +257 -0
  21. package/dist/condition-gate.js.map +1 -0
  22. package/dist/debug-state.d.ts +102 -0
  23. package/dist/debug-state.d.ts.map +1 -0
  24. package/dist/debug-state.js +135 -0
  25. package/dist/debug-state.js.map +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/operations/TaskGraphDebugOperations.d.ts +99 -0
  31. package/dist/operations/TaskGraphDebugOperations.d.ts.map +1 -0
  32. package/dist/operations/TaskGraphDebugOperations.js +310 -0
  33. package/dist/operations/TaskGraphDebugOperations.js.map +1 -0
  34. package/dist/operations/TaskGraphOperations.d.ts +20 -2
  35. package/dist/operations/TaskGraphOperations.d.ts.map +1 -1
  36. package/dist/operations/TaskGraphOperations.js +47 -8
  37. package/dist/operations/TaskGraphOperations.js.map +1 -1
  38. package/dist/settlement-rescue.d.ts +85 -0
  39. package/dist/settlement-rescue.d.ts.map +1 -0
  40. package/dist/settlement-rescue.js +119 -0
  41. package/dist/settlement-rescue.js.map +1 -0
  42. package/dist/task-graph-kick.d.ts +3 -0
  43. package/dist/task-graph-kick.d.ts.map +1 -0
  44. package/dist/task-graph-kick.js +17 -0
  45. package/dist/task-graph-kick.js.map +1 -0
  46. package/dist/task-predicates.d.ts +77 -0
  47. package/dist/task-predicates.d.ts.map +1 -0
  48. package/dist/task-predicates.js +75 -0
  49. package/dist/task-predicates.js.map +1 -0
  50. package/dist/types.d.ts +110 -1
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/package.json +12 -11
package/LICENSE CHANGED
@@ -1,7 +1,183 @@
1
- ISC License
1
+ Business Source License 1.1
2
2
 
3
- Copyright (c) 2023 MemberJunction
3
+ License text copyright (c) 2024 MariaDB plc, All Rights Reserved.
4
+ "Business Source License" is a trademark of MariaDB plc.
4
5
 
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
+ -----------------------------------------------------------------------------
6
7
 
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.
8
+ Parameters
9
+
10
+ Licensor: Blue Cypress, Inc.
11
+
12
+ Licensed Work: MemberJunction.
13
+ The Licensed Work is (c) 2023-2026 Blue Cypress, Inc.
14
+
15
+ Additional Use Grant: Subject to the terms of this License, Licensor grants
16
+ you the following additional rights to make Production
17
+ Use of the Licensed Work.
18
+
19
+ 1. Internal Use
20
+
21
+ You may make production use of the Licensed Work for
22
+ your own internal business or organizational operations.
23
+
24
+ 2. Nonprofit Use
25
+
26
+ If you are a Nonprofit, you may make production use of
27
+ the Licensed Work for the operations and activities of
28
+ your Organizational Family.
29
+
30
+ 3. MemberJunction Certified Program Use
31
+
32
+ If you are authorized by Licensor under the
33
+ MemberJunction Certified Program to provide professional
34
+ services using the Licensed Work, you may make
35
+ production use of the Licensed Work in providing such
36
+ professional services to a client, provided that:
37
+
38
+ (a) the Licensed Work is deployed in, and the applicable
39
+ production use occurs within, an environment owned,
40
+ leased, licensed, subscribed to, or otherwise controlled
41
+ by that client; and
42
+
43
+ (b) the production use is for that client's own internal
44
+ business or organizational operations or is otherwise
45
+ independently permitted to that client under this
46
+ Additional Use Grant.
47
+
48
+ 4. Definitions Applicable to the Additional Use Grant
49
+
50
+ "Affiliate" means, with respect to a specified Person,
51
+ any other Person that directly or indirectly Controls,
52
+ is Controlled by, or is under common Control with such
53
+ specified Person.
54
+
55
+ "Control" (including the terms "Controls," "Controlled
56
+ by," and "under common Control with") means the direct
57
+ or indirect possession of the power to direct or cause
58
+ the direction of the management and policies of a
59
+ Person, whether through ownership of voting interests,
60
+ by contract, or otherwise.
61
+
62
+ "Organizational Family" means, with respect to a Person,
63
+ (a) such Person and its Affiliates, and (b) any
64
+ nonprofit organization, governmental entity, chapter,
65
+ division, local affiliate, regional affiliate, state
66
+ affiliate, national affiliate, or other entity that is
67
+ formally affiliated with such Person through governing
68
+ documents, a charter, bylaws, a membership agreement, or
69
+ another written organizational instrument, and is
70
+ recognized under such documents as part of the same
71
+ organizational structure.
72
+
73
+ "Nonprofit" means a Person recognized by the Internal
74
+ Revenue Service as exempt from federal income taxation
75
+ under Section 501(c)(3), 501(c)(4), 501(c)(5), or
76
+ 501(c)(6) of the Internal Revenue Code, or a foreign
77
+ organization recognized under substantially equivalent
78
+ laws.
79
+
80
+ A Person claiming eligibility as a Nonprofit shall, upon
81
+ Licensor's reasonable request, provide documentation
82
+ reasonably sufficient to demonstrate that it qualifies
83
+ as a Nonprofit. If such Person materially misrepresents,
84
+ or is unable to demonstrate, its qualification as a
85
+ Nonprofit, the rights granted to such Person under
86
+ Section 2 of this Additional Use Grant shall terminate.
87
+
88
+ "MemberJunction Certified Program" means Licensor's
89
+ then-current program for certifying and authorizing a
90
+ Person to provide professional services using the
91
+ Licensed Work.
92
+
93
+ "Person" means any individual, corporation, limited
94
+ liability company, partnership, association, nonprofit
95
+ organization, governmental entity, or other legal or
96
+ organizational entity.
97
+
98
+ Change Date: Four (4) years from the date the Licensed Work is first
99
+ made available.
100
+
101
+ Change License: MIT License.
102
+
103
+ For information about alternative licensing arrangements for the Licensed
104
+ Work, please contact Blue Cypress, Inc.
105
+
106
+ -----------------------------------------------------------------------------
107
+
108
+ Terms
109
+
110
+ The Licensor hereby grants you the right to copy, modify, create derivative
111
+ works, redistribute, and make non-production use of the Licensed Work. The
112
+ Licensor may make an Additional Use Grant, above, permitting limited
113
+ production use.
114
+
115
+ Effective on the Change Date, or the fourth anniversary of the first publicly
116
+ available distribution of a specific version of the Licensed Work under this
117
+ License, whichever comes first, the Licensor hereby grants you rights under
118
+ the terms of the Change License, and the rights granted in the paragraph
119
+ above terminate.
120
+
121
+ If your use of the Licensed Work does not comply with the requirements
122
+ currently in effect as described in this License, you must purchase a
123
+ commercial license from the Licensor, its affiliated entities, or authorized
124
+ resellers, or you must refrain from using the Licensed Work.
125
+
126
+ All copies of the original and modified Licensed Work, and derivative works
127
+ of the Licensed Work, are subject to this License. This License applies
128
+ separately for each version of the Licensed Work and the Change Date may vary
129
+ for each version of the Licensed Work released by Licensor.
130
+
131
+ You must conspicuously display this License on each original or modified copy
132
+ of the Licensed Work. If you receive the Licensed Work in original or
133
+ modified form from a third party, the terms and conditions set forth in this
134
+ License apply to your use of that work.
135
+
136
+ Any use of the Licensed Work in violation of this License will automatically
137
+ terminate your rights under this License for the current and all other
138
+ versions of the Licensed Work.
139
+
140
+ This License does not grant you any right in any trademark or logo of
141
+ Licensor or its affiliates (provided that you may use a trademark or logo of
142
+ Licensor as expressly required by this License).
143
+
144
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
145
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
146
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
147
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
148
+ TITLE.
149
+
150
+ MariaDB hereby grants you permission to use this License's text to license
151
+ your works, and to refer to it using the trademark "Business Source License",
152
+ as long as you comply with the Covenants of Licensor below.
153
+
154
+ -----------------------------------------------------------------------------
155
+
156
+ Covenants of Licensor
157
+
158
+ In consideration of the right to use this License's text and the "Business
159
+ Source License" name and trademark, Licensor covenants to MariaDB, and to all
160
+ other recipients of the licensed work to be provided by Licensor:
161
+
162
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
163
+ or a license that is compatible with GPL Version 2.0 or a later version,
164
+ where "compatible" means that software provided under the Change License
165
+ can be included in a program with software provided under GPL Version 2.0
166
+ or a later version. Licensor may specify additional Change Licenses without
167
+ limitation.
168
+
169
+ 2. To either: (a) specify an additional grant of rights to use that does not
170
+ impose any additional restriction on the right granted in this License, as
171
+ the Additional Use Grant; or (b) insert the text "None".
172
+
173
+ 3. To specify a Change Date.
174
+
175
+ 4. Not to modify this License in any other way.
176
+
177
+ -----------------------------------------------------------------------------
178
+
179
+ Notice
180
+
181
+ The Business Source License (this document, or the "License") is not an Open
182
+ Source license. However, the Licensed Work will eventually be made available
183
+ under an Open Source License, as stated in this License.
package/README.md CHANGED
@@ -5,7 +5,8 @@ that turn a node into work.
5
5
 
6
6
  > **New to workflows?** Start with the [Workflows and Task Graphs
7
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.
8
+ > use one, and every rule that decides what happens next. To **step a live run**, see the
9
+ > [Workflow Debugger Guide](../../guides/WORKFLOW_DEBUGGER_GUIDE.md). This README is the package tour.
9
10
 
10
11
  ---
11
12
 
@@ -72,6 +73,8 @@ without standing up the agent framework.
72
73
  | `DispatcherConditionEvaluator.ts` | Edge conditions, over the superset context both dialects can read. |
73
74
  | `TaskGraphSubmitterImpl.ts` | Registers the durable submitter under the `ClassFactory` seam. |
74
75
  | `operations/` | The `TaskGraph.*` remote operations. |
76
+ | `debug-state.ts` | Durable `$.debug` bag — pause, breakpoints, step allowance, `skipBreakpointTaskID`, edge overrides. Pure claim-gate. |
77
+ | `task-graph-kick.ts` | Process-local kick set. `Start()` registers; `Submit` pokes every running dispatcher so the first pass is immediate. |
75
78
 
76
79
  ---
77
80
 
@@ -146,6 +149,30 @@ await dispatcher.Start();
146
149
  Without `LoadTaskGraphOperations()` nothing registers the submitter, and
147
150
  `GetTaskGraphSubmitter()` returns `null` — which callers must report rather than swallow.
148
151
 
152
+ `Start()` registers a kick and fires one immediately. Do not wait for the first `setInterval`
153
+ tick — that is a 5s dead pause after Debug/Run. Submit cannot import the dispatcher instance
154
+ (submit and execute are separate halves), so it calls `KickTaskGraphDispatchers()` instead.
155
+
156
+ ---
157
+
158
+ ## Debugging a live graph
159
+
160
+ Every control is a **gate on claiming**, never on running. State lives under `$.debug` on the
161
+ parent task's `InputPayload`. The debugger UI and the dispatcher never talk directly — they
162
+ rendezvous on the row. See [`debug-state.ts`](src/debug-state.ts) and the
163
+ [Workflow Debugger Guide](../../guides/WORKFLOW_DEBUGGER_GUIDE.md).
164
+
165
+ - **Start-paused.** Submit writes `paused: true` before any dispatcher has seen the graph.
166
+ - **Breakpoints.** The claim gate treats an armed, eligible task like a hold. Resume from a
167
+ breakpoint stamps `skipBreakpointTaskID` so that one claim is allowed through; without it the
168
+ next poll re-hits the same Pending row. `ForEach` is one task — one Continue runs the loop.
169
+ - **Edge overrides.** Authored `'true'` / `'false'` on a `TaskDependency` id, honored by every
170
+ instance, survive a restart.
171
+ - **Kick.** After Submit (and on `Start()`), `KickTaskGraphDispatchers()` runs a pass now.
172
+
173
+ The Angular drop-in is `<mj-task-graph-debugger>` in
174
+ [`@memberjunction/ng-task-graph-editor`](../Angular/Generic/task-graph-editor/README.md).
175
+
149
176
  ---
150
177
 
151
178
  ## Cost rollup
@@ -179,6 +206,8 @@ real rollup.
179
206
  ## Related
180
207
 
181
208
  - [Workflows and Task Graphs Guide](../../guides/WORKFLOW_AND_TASK_GRAPH_GUIDE.md) — start here
209
+ - [Workflow Debugger Guide](../../guides/WORKFLOW_DEBUGGER_GUIDE.md) — step a live run
210
+ - [`@memberjunction/ng-task-graph-editor`](../Angular/Generic/task-graph-editor/README.md) — canvas and drop-in debugger
182
211
  - [`@memberjunction/ai-core-plus`](../AI/CorePlus) — the spec, validator, compiler, pure algorithms,
183
212
  payload mapping and layout
184
213
  - [`@memberjunction/ai-agents`](../AI/Agents) — the agent framework and `FlowAgentType`
@@ -22,7 +22,44 @@
22
22
  * @module @memberjunction/task-graph
23
23
  */
24
24
  import { IMetadataProvider, UserInfo } from '@memberjunction/core';
25
+ import { type TerminalTaskGraphStatus } from '@memberjunction/ai-core-plus';
25
26
  import { ReconciliationEvent } from './types.js';
27
+ /**
28
+ * A value one debug-bag field is being set to.
29
+ *
30
+ * Discriminated so the statement renders each with the right JSON type — a boolean stored as the
31
+ * string `"true"` reads back as truthy-but-wrong, and an object stored as a string reads back as a
32
+ * string. `null` deletes the key.
33
+ */
34
+ export type TaskGraphDebugFieldValue = {
35
+ Kind: 'null';
36
+ } | {
37
+ Kind: 'bool';
38
+ Value: boolean;
39
+ } | {
40
+ Kind: 'string';
41
+ Value: string;
42
+ }
43
+ /** Pre-serialized JSON for an object or array. */
44
+ | {
45
+ Kind: 'json';
46
+ Value: string;
47
+ };
48
+ /** One field of the debug bag, addressed by its JSON path. */
49
+ export type TaskGraphDebugFieldWrite = {
50
+ Path: string;
51
+ Value: TaskGraphDebugFieldValue;
52
+ };
53
+ /**
54
+ * Every object path that must exist for a JSON path to be writable — i.e. its proper prefixes,
55
+ * excluding the root and the leaf itself.
56
+ *
57
+ * `$.debug.edgeOverrides."abc"` → `['$.debug', '$.debug.edgeOverrides']`.
58
+ *
59
+ * Exported and pure because the rule ("JSON_MODIFY does not create intermediate objects") is the
60
+ * kind of database behaviour that is easy to assume wrongly and cheap to pin with a test.
61
+ */
62
+ export declare function ContainingPaths(path: string): string[];
26
63
  /** Fields the claim protocol needs from a candidate task. */
27
64
  export type ClaimableTask = {
28
65
  ID: string;
@@ -40,6 +77,29 @@ export type ClaimableTask = {
40
77
  * changed underneath it, which is precisely the race being defended against. Every method here is a
41
78
  * single statement; nothing reads-then-writes.
42
79
  */
80
+ /**
81
+ * Statuses a graph parent has stopped moving from — the single source of truth.
82
+ *
83
+ * `Blocked` is INCLUDED: `ComputeParentRollup` returns it as settled, so a
84
+ * failure-blocked graph is as settled as a completed one. Leaving it out left a Blocked settlement
85
+ * unprotected from overwrite AND invisible to the rescue sweep — a stranded run with extra steps.
86
+ *
87
+ * Exported because the dispatcher's sweep filters on the same set. Two lists that must agree is how
88
+ * a graph becomes invisible to the machinery meant to rescue it.
89
+ */
90
+ export declare const TERMINAL_PARENT_STATUSES: readonly ["Complete", "Failed", "Cancelled", "Skipped", "Blocked"];
91
+ export type TerminalParentStatus = TerminalTaskGraphStatus;
92
+ /**
93
+ * The only status a *progress* write may set.
94
+ *
95
+ * Typed rather than left as a string so the split between the two parent writes is enforced instead
96
+ * of remembered: settling is a once-only guarded transition with a completion timestamp, and it goes
97
+ * through {@link TaskClaimStore.TrySettleParent}. Handing a terminal status to the progress method
98
+ * is now a compile error rather than a graph that settles without a `CompletedAt`.
99
+ */
100
+ export type NonTerminalParentStatus = 'In Progress';
101
+ /** The same set as a SQL literal list, so the guards and the sweep cannot drift. */
102
+ export declare const TERMINAL_PARENT_STATUS_SQL: string;
43
103
  export declare class TaskClaimStore {
44
104
  private readonly instanceID;
45
105
  private readonly claimTTLSeconds;
@@ -47,6 +107,30 @@ export declare class TaskClaimStore {
47
107
  private sql;
48
108
  /** Schema-qualified `Task` table for the provider's configured core schema. */
49
109
  private taskTable;
110
+ private agentRunTable;
111
+ /**
112
+ * Writes a graph's cost rollup onto the submitting run, those four columns and no others.
113
+ *
114
+ * **The full-row `Save()` this replaces could revert a peer's settle** (C4). Two instances
115
+ * entering the settled branch for one graph is by design, so instance B's rollup — loaded before
116
+ * A settled the run — would write back `Paused` over A's `Completed`, along with every other
117
+ * column it had read. And a crash between this write and the same pass's lifecycle write left
118
+ * the run `Paused` under a claimed marker, which no sweep re-enters.
119
+ */
120
+ TrySetRunCostRollup(provider: IMetadataProvider, runID: string, totals: {
121
+ Cost: number | null;
122
+ Tokens: number | null;
123
+ PromptTokens: number | null;
124
+ CompletionTokens: number | null;
125
+ }, contextUser: UserInfo): Promise<boolean>;
126
+ /**
127
+ * Settles a parked agent run, guarded on it still being parked.
128
+ *
129
+ * Same reasoning as the rollup above and as every parent write since Round 1: a full-row save
130
+ * carries a whole stale snapshot, and the `Paused` predicate makes the transition once-only
131
+ * across instances rather than last-write-wins.
132
+ */
133
+ TrySettleRun(provider: IMetadataProvider, runID: string, succeeded: boolean, errorMessage: string | null, contextUser: UserInfo): Promise<boolean>;
50
134
  /**
51
135
  * Attempts to claim one task.
52
136
  *
@@ -98,10 +182,15 @@ export declare class TaskClaimStore {
98
182
  * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
99
183
  * them up.
100
184
  *
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.
185
+ * **Scoped to tasks a dispatcher executes**, via the one shared predicate — see `task-predicates`.
186
+ * Expressed that way rather than as a list of the runner columns that happened to exist when this
187
+ * was written: the earlier form named `AgentID` and `ActionID` only, and the day `PromptID`
188
+ * arrived, a crashed prompt task became unrecoverable and undiagnosable in the same stroke.
189
+ *
190
+ * **Tasks a person completes are exempt.** One never carries a claim, so `In Progress` with no
191
+ * claim is its *legitimate* parked shape — an approval waiting on someone. Normalizing it would
192
+ * reset that approval out from under the user. Their lifecycle is driven by `DueAt` notification
193
+ * and escalation, never by claim expiry.
105
194
  *
106
195
  * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
107
196
  * slow-but-healthy task from being executed twice.
@@ -116,6 +205,289 @@ export declare class TaskClaimStore {
116
205
  * excluded because for them this shape is legitimate, not anomalous.
117
206
  */
118
207
  FindOrphanedInProgress(provider: IMetadataProvider, contextUser: UserInfo): Promise<ReconciliationEvent[]>;
208
+ /**
209
+ * Writes a graph parent's terminal status, and only if it is not already terminal.
210
+ *
211
+ * **Why this is not `parent.Save()`.** `GenerateSaveSQL` sends every updateable column on every
212
+ * save, not just the dirty ones — so a full-row save carries the whole in-memory snapshot,
213
+ * including `InputPayload`. Two instances polling the same settling graph both compute the
214
+ * terminal rollup; if one claims the continuation marker (written into that JSON bag) and the
215
+ * other then saves its pre-marker snapshot, **the marker is erased** and the settlement is
216
+ * delivered a second time. For `reinvoke` that is a second billed agent turn for one settlement
217
+ * — precisely the failure P4 exists to prevent, reintroduced through a column nobody thought
218
+ * they were writing.
219
+ *
220
+ * Column-scoped and guarded, per the doctrine every task transition already follows: touch
221
+ * `Status`/`PercentComplete`/`CompletedAt` and nothing else, and only from a non-terminal state.
222
+ * The second instance's write becomes a no-op instead of a rewind.
223
+ *
224
+ * @returns true when this call moved the parent to terminal; false when it was already terminal
225
+ * (someone else settled it) or the write failed
226
+ */
227
+ TrySettleParent(provider: IMetadataProvider, parentTaskID: string, status: TerminalParentStatus, percentComplete: number, contextUser: UserInfo): Promise<boolean>;
228
+ /**
229
+ * Updates a graph parent's in-flight progress — column-scoped, and refused once it is terminal.
230
+ *
231
+ * **The race this closes needs no exotic timing.** Instance A loads the graph while a child is
232
+ * still In Progress and computes a non-terminal rollup. Instance B loads after that child
233
+ * finishes, settles the parent and claims the continuation. A's full-row progress `Save()` then
234
+ * lands: `Status` reverts to non-terminal *and* A's pre-marker `InputPayload` snapshot erases
235
+ * the marker. The next pass finds a non-terminal parent with a terminal rollup and an absent
236
+ * marker — so it settles again and delivers again. That is the duplicate `reinvoke` P4 exists to
237
+ * prevent, arriving through the last unguarded window.
238
+ *
239
+ * "These writes happen before settlement" is true per instance and false across instances, which
240
+ * is exactly the kind of timing argument a guard replaces with a structural one.
241
+ */
242
+ TryUpdateParentProgress(provider: IMetadataProvider, parentTaskID: string, status: NonTerminalParentStatus, percentComplete: number, contextUser: UserInfo): Promise<boolean>;
243
+ /**
244
+ * Stamps a graph parent's start time, once, without touching anything else.
245
+ *
246
+ * Same reason as {@link TrySettleParent}: a full-row `Save()` here would carry the whole
247
+ * in-memory snapshot including `InputPayload`, so stamping a start time could erase a
248
+ * continuation marker another instance had just claimed. Guarded on `StartedAt IS NULL` so it is
249
+ * naturally once-only and safe to call on every pass.
250
+ */
251
+ TryStampParentStart(provider: IMetadataProvider, parentTaskID: string, startedAt: Date, contextUser: UserInfo): Promise<boolean>;
252
+ /**
253
+ * Claims the right to deliver a graph's continuation — exactly once, across every instance.
254
+ *
255
+ * **What this replaces.** `claimContinuation` was Load → check the marker → `BaseEntity.Save()`:
256
+ * an unconditional last-write-wins UPDATE. Two dispatchers polling the same settled graph inside
257
+ * one interval both read "no marker", both saved, and both delivered. The comments called it a
258
+ * compare-and-swap; it was read-check-write. Every *task* transition in this store is a guarded
259
+ * single statement for exactly this reason — the continuation marker was the one transition that
260
+ * was not.
261
+ *
262
+ * The marker lives inside the parent's `InputPayload` JSON bag rather than a column, so the
263
+ * guard is a JSON predicate. That keeps one representation for writer and reader: this statement
264
+ * writes it, `ParseTaskGraphParentMetadata` reads it, and a graph settled before this existed is
265
+ * decided by the same parser as one settled after — which a new column plus a backfill could not
266
+ * promise.
267
+ *
268
+ * Timestamps are ISO 8601 UTC because the TS reader parses them; `JSON_MODIFY` on a row whose
269
+ * payload is absent or unparseable writes nothing and the rowcount says so, which is the honest
270
+ * outcome — a graph we cannot read metadata for is one we must not deliver for.
271
+ *
272
+ * `workflowTaskTypeID` is REQUIRED rather than optional because this statement injects keys into
273
+ * a row's `InputPayload`. `MJ: Tasks` holds conversation tasks and users' own to-dos as well as
274
+ * workflow graphs; a mis-targeted claim would silently edit somebody's payload. Passing the
275
+ * discriminator is not a filter the caller may forget — it is the caller stating which family of
276
+ * task it believes it is writing to, and the statement refusing if it is wrong.
277
+ *
278
+ * @param deliveredAs how the settlement is being delivered, recorded alongside the marker so an
279
+ * expired settlement is distinguishable from a delivered one after the fact
280
+ * @returns true when this instance won the right to deliver
281
+ */
282
+ TryClaimContinuation(provider: IMetadataProvider, parentTaskID: string, deliveredAs: 'delivered' | 'expired' | 'cancelled', workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
283
+ /**
284
+ * Skips one task, refusing if anything has taken it since the caller looked.
285
+ *
286
+ * **Why this cannot be a `Save()`** — and R3-1 is the proof that the earlier reasoning was wrong.
287
+ * The early-finish path skipped siblings with a full-row `BaseEntity.Save()` against a snapshot
288
+ * taken before the loop began, justified by "the siblings are Pending and unclaimed until the
289
+ * skip lands". They are not: `executeClaimed` is not awaited, so this instance's own next poll
290
+ * tick runs concurrently with the loop, and a sibling can be claimed and STARTED between the
291
+ * snapshot and its own write. The full-row save then overwrote `In Progress` back to `Skipped`
292
+ * and cleared `ClaimedBy` mid-execution — the agent's real side effects had already fired, its
293
+ * completion was refused by the claim guard, and its output was discarded. The graph settled
294
+ * `Complete` with no record anywhere that the step ran.
295
+ *
296
+ * **The status predicate is `Status='Pending'` alone, deliberately.** `TryClaim` moves a task
297
+ * to `In Progress` in the same statement that stamps `ClaimedBy`, so a task an executor holds is
298
+ * never `Pending` — the status IS the claim test. Adding `ClaimedBy IS NULL` would look like
299
+ * defence in depth and would instead break a real case: a notified human task carries a marker
300
+ * in `ClaimedBy` while still `Pending`, and those must stay skippable.
301
+ *
302
+ * **Type-scoped, like every other write in this store that a caller-supplied ID can reach.**
303
+ * `MJ: Tasks` also holds conversation tasks and users' personal to-dos; without the
304
+ * discriminator an operator verb pointed at a mis-derived (or hostile) ID could write `Skipped`
305
+ * onto somebody's to-do. The engine-internal caller (`endGraphEarly`) derives its IDs from a
306
+ * workflow parent's own children, but it pays the same predicate — one statement, one contract.
307
+ *
308
+ * @returns true when this call is the one that skipped it; false means something else got there
309
+ */
310
+ TrySkipPending(provider: IMetadataProvider, taskID: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
311
+ /**
312
+ * Stamps the human-notified marker, once, without touching anything else.
313
+ *
314
+ * The marker lives in `ClaimedBy` because a human task has no executor claim, and it exists to
315
+ * stop the notify path re-raising on every poll. It was written with a full-row `Save()` against
316
+ * a snapshot — so it could revert a status the row had reached since, and two instances could
317
+ * both write it after both having seen it absent. Guarded on the marker being unset, it is
318
+ * naturally once-only and the rowcount says which instance did it.
319
+ */
320
+ TryMarkHumanNotified(provider: IMetadataProvider, taskID: string, marker: string, contextUser: UserInfo): Promise<boolean>;
321
+ /**
322
+ * Cancels one task, refusing if it settled while the caller was looking elsewhere.
323
+ *
324
+ * **The terminal check has to be IN the statement.** `Cancel` loaded every child, tested the
325
+ * terminal set against that in-memory snapshot, and wrote `Status='Cancelled'` with a full-row
326
+ * `BaseEntity.Save()` — an unconditional UPDATE sending every updateable column against a
327
+ * PK-only predicate. A child whose executor's guarded `CompleteClaimed` landed between the load
328
+ * and its save had its entire outcome overwritten: `Complete` back to `Cancelled`,
329
+ * `OutputPayload` to NULL (the null-clear companions make those explicit clears),
330
+ * `AgentRunID`/`CompletedAt`/runtime `Configuration` reverted, and stale claim columns
331
+ * re-instated on a terminal row.
332
+ *
333
+ * The moment users cancel is exactly the moment tasks are running, so this is not a narrow
334
+ * window. The reverse ordering was always safe — `CompleteClaimed`'s own predicate refuses a
335
+ * cancelled row — so the hazard lived entirely in this write.
336
+ *
337
+ * @returns true when this call cancelled it; false means it had already settled
338
+ */
339
+ TryCancelTask(provider: IMetadataProvider, taskID: string, contextUser: UserInfo): Promise<boolean>;
340
+ /**
341
+ * Records, durably and once, that a graph is finishing early.
342
+ *
343
+ * **The declaration has to outlive the deciding instance's memory.** An early finish is decided
344
+ * by one task's result (`result.ChatMessage`) and nothing else in the system knows: skip seeds
345
+ * are derived from durable condition and exclusive-group state, so no claim filter on any
346
+ * instance — including the deciding one, whose poll loop runs concurrently — can tell that the
347
+ * remaining steps are about to be skipped. Writing it here first is what lets
348
+ * `loadGraphState` fold those steps into the claim filter, closing the window for everyone
349
+ * rather than narrowing it for one.
350
+ *
351
+ * Guarded and once-only for the same reason the continuation marker is: two tasks can end the
352
+ * same flow, and the first declaration is the one that counts. Type-scoped like every other
353
+ * statement here that writes into a payload column.
354
+ *
355
+ * @returns true when this call is the one that declared it
356
+ */
357
+ TryDeclareEarlyFinish(provider: IMetadataProvider, parentTaskID: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
358
+ /**
359
+ * Records why a graph ended early, writing that column and no other.
360
+ *
361
+ * The hazard is the one {@link TrySettleParent} exists for, reached by a different route. A task
362
+ * that ends the flow early skips its siblings, which makes the graph fully terminal — so another
363
+ * instance's very next poll can settle it and claim the continuation marker. The old code had
364
+ * already loaded the parent by then and finished with a full-row `Save()`, which would write back
365
+ * the pre-settle snapshot: status reverted to `In Progress`, marker gone, graph delivered twice.
366
+ *
367
+ * No status predicate here, unlike the other writes: the early-finish message is the truthful
368
+ * summary whether or not the graph has settled since, and two tasks ending the same flow both
369
+ * describe it correctly. The bug was never the value — it was the other columns riding along.
370
+ *
371
+ * Type-scoped for the same reason the claim is: every statement in this store that writes into a
372
+ * payload column states which family of task it means, so a mis-derived parent ID cannot edit a
373
+ * conversation task or somebody's to-do.
374
+ */
375
+ TrySetParentOutput(provider: IMetadataProvider, parentTaskID: string, outputPayload: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
376
+ /**
377
+ * Clears a graph's debug state entirely — the "stop debugging this run" write.
378
+ *
379
+ * Whole-bag, and safe to be: deleting `$.debug` is the one operation that genuinely owns every
380
+ * field in it. Every PARTIAL change goes through {@link TryWriteDebugFields}, because a
381
+ * read-merge-write of the whole bag puts back whatever the fields a verb does not own held at
382
+ * read time — most sharply resurrecting a step allowance the dispatcher consumed in between.
383
+ */
384
+ TryClearDebugState(provider: IMetadataProvider, parentTaskID: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
385
+ /**
386
+ * One field of the debug bag, as a value the statement can write.
387
+ *
388
+ * Typed rather than a raw SQL fragment so a caller cannot inject one: the shape decides how the
389
+ * value is rendered, and every string goes through {@link escape}.
390
+ */
391
+ static DebugField(path: string, value: TaskGraphDebugFieldValue): TaskGraphDebugFieldWrite;
392
+ /**
393
+ * Writes named fields of a graph's debug bag, leaving every other field alone.
394
+ *
395
+ * **Why field-scoped rather than rewriting `$.debug`.** A read-merge-write of the whole bag is
396
+ * the same stale-snapshot hazard as a full-row save, one level down: a verb that reads the bag,
397
+ * merges its own change, and writes the result puts back whatever the fields it does NOT own
398
+ * held at read time. The sharp case is the step allowance — if the dispatcher consumes it
399
+ * between a `SetBreakpoints` read and its write, the rewrite *resurrects* the consumed
400
+ * allowance and one press of Step releases two waves, straight through the CAS that exists to
401
+ * prevent exactly that. Writing only the paths a verb owns removes the class rather than
402
+ * narrowing the window.
403
+ *
404
+ * Paths are nested `JSON_MODIFY` calls, so the whole set lands in one statement.
405
+ */
406
+ TryWriteDebugFields(provider: IMetadataProvider, parentTaskID: string, fields: readonly TaskGraphDebugFieldWrite[], workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
407
+ /**
408
+ * Wraps a payload expression so every object CONTAINING one of these paths exists.
409
+ *
410
+ * `JSON_MODIFY` does not create intermediate objects: writing `$.debug.paused` into a payload
411
+ * with no `debug` key, or `$.debug.edgeOverrides."<id>"` with no override map yet, silently
412
+ * changes nothing — which for a control verb means the write reports success (rowcount 1, the
413
+ * row WAS updated, just not the way anyone meant) and the workflow never pauses. A graph only
414
+ * acquires a `debug` key the first time somebody debugs it, so this is the NORMAL first call,
415
+ * not an edge case.
416
+ *
417
+ * This hazard arrived WITH field-scoped writes and is the price of them: the whole-bag write
418
+ * they replaced targeted `$.debug`, one level down from a root that always exists, so it
419
+ * created the containing object as a side effect of every verb. Field-scoping is still the
420
+ * right trade — it removes the step-resurrection class outright — but it moves the
421
+ * container's existence from implicit to something this method has to guarantee.
422
+ *
423
+ * Each containing object is created only when absent, shallowest first, so an existing bag is
424
+ * never replaced.
425
+ */
426
+ private ensureObjects;
427
+ /** Renders one debug value as a SQL literal `JSON_MODIFY` will store with the right JSON type. */
428
+ private renderDebugValue;
429
+ /**
430
+ * Consumes a paused graph's one-shot step allowance — exactly once, across every instance.
431
+ *
432
+ * The predicate `$.debug.step IS NOT NULL` is the whole contract: two dispatchers polling the
433
+ * same paused graph inside one interval both see the allowance, but only one statement clears it
434
+ * and sees rowcount 1. The loser claims nothing and waits for the next allowance, so "step" can
435
+ * never release two waves.
436
+ */
437
+ TryConsumeStepMarker(provider: IMetadataProvider, parentTaskID: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
438
+ /**
439
+ * Pauses a graph because an eligible task hit a breakpoint — once, whichever instance sees it
440
+ * first.
441
+ *
442
+ * Guarded on "not already paused" so two instances arriving at the same breakpoint in the same
443
+ * interval produce one `BreakpointHit` announcement, not two. The graph's existing breakpoint
444
+ * list and edge overrides are untouched — only the pause fields are written.
445
+ *
446
+ * The `$.debug` object is created when absent, for the same reason the field-scoped writes need
447
+ * it: `JSON_MODIFY` will not create a missing container, so without this the pause would report
448
+ * success and the workflow would run straight through its breakpoint. Reachable here only since
449
+ * the writes became field-scoped — the whole-bag write this replaced created `$.debug` on the
450
+ * way past, so a breakpoint could not exist without its container already being there.
451
+ */
452
+ TryPauseAtBreakpoint(provider: IMetadataProvider, parentTaskID: string, breakpointTaskID: string, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
453
+ /**
454
+ * Replaces a task's input, guarded on the status the caller believes it is in.
455
+ *
456
+ * **Why this is a guarded statement and not `task.Save()`.** The obvious shape — load, check
457
+ * `Status === 'Pending'` in memory, save — is an unconditional full-row UPDATE carrying the
458
+ * whole loaded snapshot. A task claimed between the load and the save has its `Status`,
459
+ * `ClaimedBy` and `ClaimExpiresAt` reverted to that snapshot *while its body executes*, after
460
+ * which a second instance claims it again and the step runs twice. That is the stale-snapshot
461
+ * class this file's header exists to prevent, and it does not become safe because the window is
462
+ * small — the dispatcher polls every few seconds.
463
+ *
464
+ * `expectedStatus` is a parameter because two verbs need it: editing the brief of a step that
465
+ * has not started (`Pending`) and correcting the brief of one that failed, on the way into a
466
+ * retry (`Failed`).
467
+ */
468
+ TryUpdateInputPayload(provider: IMetadataProvider, taskID: string, inputPayload: string | null, expectedStatus: 'Pending' | 'Failed', workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
469
+ /**
470
+ * Marks a task Complete with an operator-supplied output — the escape hatch for a wedged or
471
+ * externally-resolved step.
472
+ *
473
+ * The guard is deliberately narrow: `Pending`, `Failed`, `Blocked`, or `In Progress` **with a
474
+ * lapsed claim**. A live claim means an executor is genuinely working, and force-completing
475
+ * underneath it would hand dependents an output the still-running body is about to contradict —
476
+ * that case must go through Cancel or wait for the claim to lapse. Downstream edges evaluate
477
+ * against the supplied output exactly as they would a runner's.
478
+ *
479
+ * **The lapsed-claim test uses the DATABASE clock, not this process's.** With app/DB skew — or
480
+ * skew between two app servers — a claim that is live on the clock that wrote it can read as
481
+ * expired on the clock that judges it, and this verb would then complete a task underneath a
482
+ * running executor. That interleaving is the entire reason the gate is narrow, so the gate must
483
+ * not be the thing that gets it wrong. The database is the one reference every instance shares.
484
+ * (This verb once carried a residual asymmetry — it *judged* on the database clock while
485
+ * `TryClaim` still *wrote* the lease from the claiming process's clock, trading app-vs-app skew
486
+ * for app-vs-DB skew. The claim protocol has since moved its write to `SYSUTCDATETIME()` as
487
+ * well, so both ends of the comparison now come from the one shared clock and the window is
488
+ * closed rather than relocated.)
489
+ */
490
+ TryForceComplete(provider: IMetadataProvider, taskID: string, outputPayload: string | null, workflowTaskTypeID: string, contextUser: UserInfo): Promise<boolean>;
119
491
  /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
120
492
  private affectedRows;
121
493
  private literalOrNull;