@memberjunction/task-graph 6.1.0-edge.1 → 6.1.0-edge.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +214 -0
  3. package/dist/TaskClaimStore.d.ts +387 -4
  4. package/dist/TaskClaimStore.d.ts.map +1 -1
  5. package/dist/TaskClaimStore.js +605 -20
  6. package/dist/TaskClaimStore.js.map +1 -1
  7. package/dist/TaskGraphDispatcher.d.ts +668 -5
  8. package/dist/TaskGraphDispatcher.d.ts.map +1 -1
  9. package/dist/TaskGraphDispatcher.js +2942 -127
  10. package/dist/TaskGraphDispatcher.js.map +1 -1
  11. package/dist/TaskGraphService.d.ts +364 -5
  12. package/dist/TaskGraphService.d.ts.map +1 -1
  13. package/dist/TaskGraphService.js +1039 -43
  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/TaskLoopExecutor.d.ts +62 -0
  19. package/dist/TaskLoopExecutor.d.ts.map +1 -0
  20. package/dist/TaskLoopExecutor.js +248 -0
  21. package/dist/TaskLoopExecutor.js.map +1 -0
  22. package/dist/WorkflowSpecSync.d.ts +28 -2
  23. package/dist/WorkflowSpecSync.d.ts.map +1 -1
  24. package/dist/WorkflowSpecSync.js +83 -2
  25. package/dist/WorkflowSpecSync.js.map +1 -1
  26. package/dist/condition-gate.d.ts +128 -0
  27. package/dist/condition-gate.d.ts.map +1 -0
  28. package/dist/condition-gate.js +257 -0
  29. package/dist/condition-gate.js.map +1 -0
  30. package/dist/debug-state.d.ts +102 -0
  31. package/dist/debug-state.d.ts.map +1 -0
  32. package/dist/debug-state.js +135 -0
  33. package/dist/debug-state.js.map +1 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/operations/TaskGraphDebugOperations.d.ts +99 -0
  39. package/dist/operations/TaskGraphDebugOperations.d.ts.map +1 -0
  40. package/dist/operations/TaskGraphDebugOperations.js +310 -0
  41. package/dist/operations/TaskGraphDebugOperations.js.map +1 -0
  42. package/dist/operations/TaskGraphOperations.d.ts +20 -2
  43. package/dist/operations/TaskGraphOperations.d.ts.map +1 -1
  44. package/dist/operations/TaskGraphOperations.js +51 -8
  45. package/dist/operations/TaskGraphOperations.js.map +1 -1
  46. package/dist/operations/WorkflowDraftOperation.d.ts +37 -0
  47. package/dist/operations/WorkflowDraftOperation.d.ts.map +1 -0
  48. package/dist/operations/WorkflowDraftOperation.js +141 -0
  49. package/dist/operations/WorkflowDraftOperation.js.map +1 -0
  50. package/dist/settlement-rescue.d.ts +85 -0
  51. package/dist/settlement-rescue.d.ts.map +1 -0
  52. package/dist/settlement-rescue.js +119 -0
  53. package/dist/settlement-rescue.js.map +1 -0
  54. package/dist/task-graph-kick.d.ts +3 -0
  55. package/dist/task-graph-kick.d.ts.map +1 -0
  56. package/dist/task-graph-kick.js +17 -0
  57. package/dist/task-graph-kick.js.map +1 -0
  58. package/dist/task-predicates.d.ts +77 -0
  59. package/dist/task-predicates.d.ts.map +1 -0
  60. package/dist/task-predicates.js +75 -0
  61. package/dist/task-predicates.js.map +1 -0
  62. package/dist/types.d.ts +224 -1
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +12 -8
@@ -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
  *
@@ -82,15 +166,31 @@ export declare class TaskClaimStore {
82
166
  OutputPayload?: string | null;
83
167
  ErrorMessage?: string | null;
84
168
  AgentRunID?: string | null;
169
+ /**
170
+ * The step's Configuration bag, when the run produced something that belongs in it.
171
+ *
172
+ * Written in the SAME guarded UPDATE as the rest of the outcome rather than a follow-up
173
+ * save, because a second write could land after the row was reclaimed and would then
174
+ * attribute one instance's runtime artefacts to another instance's execution.
175
+ *
176
+ * Omitted leaves the column untouched — a step whose run produces no artefacts must not
177
+ * have its authored configuration blanked as a side effect of finishing.
178
+ */
179
+ Configuration?: string | null;
85
180
  }, contextUser: UserInfo): Promise<boolean>;
86
181
  /**
87
182
  * Reclaims tasks whose claims have lapsed, returning them to `Pending` so any instance can pick
88
183
  * them up.
89
184
  *
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.
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.
94
194
  *
95
195
  * Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a
96
196
  * slow-but-healthy task from being executed twice.
@@ -105,6 +205,289 @@ export declare class TaskClaimStore {
105
205
  * excluded because for them this shape is legitimate, not anomalous.
106
206
  */
107
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>;
108
491
  /** Runs the affected-rows statement, returning 0 on error rather than throwing into the loop. */
109
492
  private affectedRows;
110
493
  private literalOrNull;
@@ -1 +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"}
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,EAAgC,KAAK,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE1G,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,MAAM,wBAAwB,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACnC,kDAAkD;GAChD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,8DAA8D;AAC9D,MAAM,MAAM,wBAAwB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,wBAAwB,CAAC;CACnC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBtD;AAED,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;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,oEAA+B,CAAC;AAErE,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAC;AAEpD,oFAAoF;AACpF,eAAO,MAAM,0BAA0B,QAA0D,CAAC;AAElG,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,OAAO,CAAC,aAAa;IAKrB;;;;;;;;OAQG;IACU,mBAAmB,CAC5B,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EACpH,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAanB;;;;;;OAMG;IACU,YAAY,CACrB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAenB;;;;;;;;;OASG;IACU,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB3G;;;;;;;;OAQG;IACU,SAAS,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAa5G;;;;;;;;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;;;;;;;;;;;;;;;;OAgBG;IACU,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IA0CrH;;;;;;;OAOG;IACU,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAqBvH;;;;;;;;;;;;;;;;;;OAkBG;IACU,eAAe,CACxB,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,oBAAoB,EAC5B,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAYnB;;;;;;;;;;;;;OAaG;IACU,uBAAuB,CAChC,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,uBAAuB,EAC/B,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAWnB;;;;;;;OAOG;IACU,mBAAmB,CAC5B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,IAAI,EACf,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAUnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACU,oBAAoB,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,EAClD,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAgBnB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACU,cAAc,CACvB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAWnB;;;;;;;;OAQG;IACU,oBAAoB,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAWnB;;;;;;;;;;;;;;;;;OAiBG;IACU,aAAa,CACtB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAUnB;;;;;;;;;;;;;;;;OAgBG;IACU,qBAAqB,CAC9B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAcnB;;;;;;;;;;;;;;;;OAgBG;IACU,kBAAkB,CAC3B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAUnB;;;;;;;OAOG;IACU,kBAAkB,CAC3B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAYnB;;;;;OAKG;WACW,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,wBAAwB;IAIjG;;;;;;;;;;;;;OAaG;IACU,mBAAmB,CAC5B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,SAAS,wBAAwB,EAAE,EAC3C,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAqBnB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,aAAa;IAgBrB,kGAAkG;IAClG,OAAO,CAAC,gBAAgB;IAWxB;;;;;;;OAOG;IACU,oBAAoB,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAanB;;;;;;;;;;;;;OAaG;IACU,oBAAoB,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAmBnB;;;;;;;;;;;;;;OAcG;IACU,qBAAqB,CAC9B,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,cAAc,EAAE,SAAS,GAAG,QAAQ,EACpC,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAYnB;;;;;;;;;;;;;;;;;;;;OAoBG;IACU,gBAAgB,CACzB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC;IAqBnB,iGAAiG;YACnF,YAAY;IAiB1B,OAAO,CAAC,aAAa;IAIrB,4FAA4F;IAC5F,OAAO,CAAC,MAAM;CAGjB"}