Member Junction
    Preparing search index...
    Index

    Constructors

    Methods

    • Records a terminal outcome and releases the claim in one guarded statement.

      Guarded on both Status='In Progress' and ClaimedBy=@me: a task that was cancelled or reassigned while running fails the predicate, so a stale executor cannot overwrite the newer decision. The caller treats rowcount 0 as "someone else owns this now" rather than an error.

      Parameters

      • provider: IMetadataProvider
      • taskID: string
      • outcome: {
            AgentRunID?: string;
            Configuration?: string;
            ErrorMessage?: string;
            OutputPayload?: string;
            Status: "Complete" | "Failed";
        }
        • OptionalAgentRunID?: string
        • OptionalConfiguration?: string

          The step's Configuration bag, when the run produced something that belongs in it.

          Written in the SAME guarded UPDATE as the rest of the outcome rather than a follow-up save, because a second write could land after the row was reclaimed and would then attribute one instance's runtime artefacts to another instance's execution.

          Omitted leaves the column untouched — a step whose run produces no artefacts must not have its authored configuration blanked as a side effect of finishing.

        • OptionalErrorMessage?: string
        • OptionalOutputPayload?: string
        • Status: "Complete" | "Failed"
      • contextUser: UserInfo

      Returns Promise<boolean>

      true when this instance's outcome was recorded

    • Returns agent tasks sitting In Progress with no claim at all.

      This is the anomalous shape D20 anticipates from a human or an agent writing Status directly. It is reported rather than silently corrected: the row is evidence of tampering or of a bug, and Record Changes already carries the audit trail. Human-assigned tasks are excluded because for them this shape is legitimate, not anomalous.

      Parameters

      Returns Promise<ReconciliationEvent[]>

    • Extends this instance's claim on a task it is actively running.

      Guarded on ClaimedBy=@me so a heartbeat can never resurrect a claim that reconciliation already released — if the sweep took the task back, the heartbeat fails and the executor learns its work is no longer owned.

      Parameters

      Returns Promise<boolean>

      true when the claim was extended; false means this instance no longer owns the task

    • Reclaims tasks whose claims have lapsed, returning them to Pending so any instance can pick them up.

      Scoped to tasks a dispatcher executes, via the one shared predicate — see task-predicates. Expressed that way rather than as a list of the runner columns that happened to exist when this was written: the earlier form named AgentID and ActionID only, and the day PromptID arrived, a crashed prompt task became unrecoverable and undiagnosable in the same stroke.

      Tasks a person completes are exempt. One never carries a claim, so In Progress with no claim is its legitimate parked shape — an approval waiting on someone. Normalizing it would reset that approval out from under the user. Their lifecycle is driven by DueAt notification and escalation, never by claim expiry.

      Only expired claims are reclaimed; a live claim is left strictly alone, which is what keeps a slow-but-healthy task from being executed twice.

      Parameters

      Returns Promise<ReconciliationEvent[]>

    • Cancels one task, refusing if it settled while the caller was looking elsewhere.

      The terminal check has to be IN the statement. Cancel loaded every child, tested the terminal set against that in-memory snapshot, and wrote Status='Cancelled' with a full-row BaseEntity.Save() — an unconditional UPDATE sending every updateable column against a PK-only predicate. A child whose executor's guarded CompleteClaimed landed between the load and its save had its entire outcome overwritten: Complete back to Cancelled, OutputPayload to NULL (the null-clear companions make those explicit clears), AgentRunID/CompletedAt/runtime Configuration reverted, and stale claim columns re-instated on a terminal row.

      The moment users cancel is exactly the moment tasks are running, so this is not a narrow window. The reverse ordering was always safe — CompleteClaimed's own predicate refuses a cancelled row — so the hazard lived entirely in this write.

      Parameters

      Returns Promise<boolean>

      true when this call cancelled it; false means it had already settled

    • Attempts to claim one task.

      The Status='Pending' predicate is the whole contract: a task another instance already moved to In Progress fails the predicate and yields rowcount 0. ClaimedBy IS NULL OR ClaimExpiresAt < now additionally lets an expired claim be taken over without a separate reconciliation pass having to run first.

      Parameters

      Returns Promise<boolean>

      true when this instance now owns the task

    • Claims the right to deliver a graph's continuation — exactly once, across every instance.

      What this replaces. claimContinuation was Load → check the marker → BaseEntity.Save(): an unconditional last-write-wins UPDATE. Two dispatchers polling the same settled graph inside one interval both read "no marker", both saved, and both delivered. The comments called it a compare-and-swap; it was read-check-write. Every task transition in this store is a guarded single statement for exactly this reason — the continuation marker was the one transition that was not.

      The marker lives inside the parent's InputPayload JSON bag rather than a column, so the guard is a JSON predicate. That keeps one representation for writer and reader: this statement writes it, ParseTaskGraphParentMetadata reads it, and a graph settled before this existed is decided by the same parser as one settled after — which a new column plus a backfill could not promise.

      Timestamps are ISO 8601 UTC because the TS reader parses them; JSON_MODIFY on a row whose payload is absent or unparseable writes nothing and the rowcount says so, which is the honest outcome — a graph we cannot read metadata for is one we must not deliver for.

      workflowTaskTypeID is REQUIRED rather than optional because this statement injects keys into a row's InputPayload. MJ: Tasks holds conversation tasks and users' own to-dos as well as workflow graphs; a mis-targeted claim would silently edit somebody's payload. Passing the discriminator is not a filter the caller may forget — it is the caller stating which family of task it believes it is writing to, and the statement refusing if it is wrong.

      Parameters

      • provider: IMetadataProvider
      • parentTaskID: string
      • deliveredAs: "delivered" | "expired" | "cancelled"

        how the settlement is being delivered, recorded alongside the marker so an expired settlement is distinguishable from a delivered one after the fact

      • workflowTaskTypeID: string
      • contextUser: UserInfo

      Returns Promise<boolean>

      true when this instance won the right to deliver

    • Clears a graph's debug state entirely — the "stop debugging this run" write.

      Whole-bag, and safe to be: deleting $.debug is the one operation that genuinely owns every field in it. Every PARTIAL change goes through TryWriteDebugFields, because a read-merge-write of the whole bag puts back whatever the fields a verb does not own held at read time — most sharply resurrecting a step allowance the dispatcher consumed in between.

      Parameters

      Returns Promise<boolean>

    • Consumes a paused graph's one-shot step allowance — exactly once, across every instance.

      The predicate $.debug.step IS NOT NULL is the whole contract: two dispatchers polling the same paused graph inside one interval both see the allowance, but only one statement clears it and sees rowcount 1. The loser claims nothing and waits for the next allowance, so "step" can never release two waves.

      Parameters

      Returns Promise<boolean>

    • Records, durably and once, that a graph is finishing early.

      The declaration has to outlive the deciding instance's memory. An early finish is decided by one task's result (result.ChatMessage) and nothing else in the system knows: skip seeds are derived from durable condition and exclusive-group state, so no claim filter on any instance — including the deciding one, whose poll loop runs concurrently — can tell that the remaining steps are about to be skipped. Writing it here first is what lets loadGraphState fold those steps into the claim filter, closing the window for everyone rather than narrowing it for one.

      Guarded and once-only for the same reason the continuation marker is: two tasks can end the same flow, and the first declaration is the one that counts. Type-scoped like every other statement here that writes into a payload column.

      Parameters

      Returns Promise<boolean>

      true when this call is the one that declared it

    • Marks a task Complete with an operator-supplied output — the escape hatch for a wedged or externally-resolved step.

      The guard is deliberately narrow: Pending, Failed, Blocked, or In Progress with a lapsed claim. A live claim means an executor is genuinely working, and force-completing underneath it would hand dependents an output the still-running body is about to contradict — that case must go through Cancel or wait for the claim to lapse. Downstream edges evaluate against the supplied output exactly as they would a runner's.

      The lapsed-claim test uses the DATABASE clock, not this process's. With app/DB skew — or skew between two app servers — a claim that is live on the clock that wrote it can read as expired on the clock that judges it, and this verb would then complete a task underneath a running executor. That interleaving is the entire reason the gate is narrow, so the gate must not be the thing that gets it wrong. The database is the one reference every instance shares. (This verb once carried a residual asymmetry — it judged on the database clock while TryClaim still wrote the lease from the claiming process's clock, trading app-vs-app skew for app-vs-DB skew. The claim protocol has since moved its write to SYSUTCDATETIME() as well, so both ends of the comparison now come from the one shared clock and the window is closed rather than relocated.)

      Parameters

      Returns Promise<boolean>

    • Stamps the human-notified marker, once, without touching anything else.

      The marker lives in ClaimedBy because a human task has no executor claim, and it exists to stop the notify path re-raising on every poll. It was written with a full-row Save() against a snapshot — so it could revert a status the row had reached since, and two instances could both write it after both having seen it absent. Guarded on the marker being unset, it is naturally once-only and the rowcount says which instance did it.

      Parameters

      Returns Promise<boolean>

    • Pauses a graph because an eligible task hit a breakpoint — once, whichever instance sees it first.

      Guarded on "not already paused" so two instances arriving at the same breakpoint in the same interval produce one BreakpointHit announcement, not two. The graph's existing breakpoint list and edge overrides are untouched — only the pause fields are written.

      The $.debug object is created when absent, for the same reason the field-scoped writes need it: JSON_MODIFY will not create a missing container, so without this the pause would report success and the workflow would run straight through its breakpoint. Reachable here only since the writes became field-scoped — the whole-bag write this replaced created $.debug on the way past, so a breakpoint could not exist without its container already being there.

      Parameters

      Returns Promise<boolean>

    • Records why a graph ended early, writing that column and no other.

      The hazard is the one TrySettleParent exists for, reached by a different route. A task that ends the flow early skips its siblings, which makes the graph fully terminal — so another instance's very next poll can settle it and claim the continuation marker. The old code had already loaded the parent by then and finished with a full-row Save(), which would write back the pre-settle snapshot: status reverted to In Progress, marker gone, graph delivered twice.

      No status predicate here, unlike the other writes: the early-finish message is the truthful summary whether or not the graph has settled since, and two tasks ending the same flow both describe it correctly. The bug was never the value — it was the other columns riding along.

      Type-scoped for the same reason the claim is: every statement in this store that writes into a payload column states which family of task it means, so a mis-derived parent ID cannot edit a conversation task or somebody's to-do.

      Parameters

      Returns Promise<boolean>

    • Writes a graph's cost rollup onto the submitting run, those four columns and no others.

      The full-row Save() this replaces could revert a peer's settle (C4). Two instances entering the settled branch for one graph is by design, so instance B's rollup — loaded before A settled the run — would write back Paused over A's Completed, along with every other column it had read. And a crash between this write and the same pass's lifecycle write left the run Paused under a claimed marker, which no sweep re-enters.

      Parameters

      • provider: IMetadataProvider
      • runID: string
      • totals: { CompletionTokens: number; Cost: number; PromptTokens: number; Tokens: number }
      • contextUser: UserInfo

      Returns Promise<boolean>

    • Writes a graph parent's terminal status, and only if it is not already terminal.

      Why this is not parent.Save(). GenerateSaveSQL sends every updateable column on every save, not just the dirty ones — so a full-row save carries the whole in-memory snapshot, including InputPayload. Two instances polling the same settling graph both compute the terminal rollup; if one claims the continuation marker (written into that JSON bag) and the other then saves its pre-marker snapshot, the marker is erased and the settlement is delivered a second time. For reinvoke that is a second billed agent turn for one settlement — precisely the failure P4 exists to prevent, reintroduced through a column nobody thought they were writing.

      Column-scoped and guarded, per the doctrine every task transition already follows: touch Status/PercentComplete/CompletedAt and nothing else, and only from a non-terminal state. The second instance's write becomes a no-op instead of a rewind.

      Parameters

      • provider: IMetadataProvider
      • parentTaskID: string
      • status: "Complete" | "Failed" | "Cancelled" | "Skipped" | "Blocked"
      • percentComplete: number
      • contextUser: UserInfo

      Returns Promise<boolean>

      true when this call moved the parent to terminal; false when it was already terminal (someone else settled it) or the write failed

    • Settles a parked agent run, guarded on it still being parked.

      Same reasoning as the rollup above and as every parent write since Round 1: a full-row save carries a whole stale snapshot, and the Paused predicate makes the transition once-only across instances rather than last-write-wins.

      Parameters

      Returns Promise<boolean>

    • Skips one task, refusing if anything has taken it since the caller looked.

      Why this cannot be a Save() — and R3-1 is the proof that the earlier reasoning was wrong. The early-finish path skipped siblings with a full-row BaseEntity.Save() against a snapshot taken before the loop began, justified by "the siblings are Pending and unclaimed until the skip lands". They are not: executeClaimed is not awaited, so this instance's own next poll tick runs concurrently with the loop, and a sibling can be claimed and STARTED between the snapshot and its own write. The full-row save then overwrote In Progress back to Skipped and cleared ClaimedBy mid-execution — the agent's real side effects had already fired, its completion was refused by the claim guard, and its output was discarded. The graph settled Complete with no record anywhere that the step ran.

      The status predicate is Status='Pending' alone, deliberately. TryClaim moves a task to In Progress in the same statement that stamps ClaimedBy, so a task an executor holds is never Pending — the status IS the claim test. Adding ClaimedBy IS NULL would look like defence in depth and would instead break a real case: a notified human task carries a marker in ClaimedBy while still Pending, and those must stay skippable.

      Type-scoped, like every other write in this store that a caller-supplied ID can reach. MJ: Tasks also holds conversation tasks and users' personal to-dos; without the discriminator an operator verb pointed at a mis-derived (or hostile) ID could write Skipped onto somebody's to-do. The engine-internal caller (endGraphEarly) derives its IDs from a workflow parent's own children, but it pays the same predicate — one statement, one contract.

      Parameters

      Returns Promise<boolean>

      true when this call is the one that skipped it; false means something else got there

    • Stamps a graph parent's start time, once, without touching anything else.

      Same reason as TrySettleParent: a full-row Save() here would carry the whole in-memory snapshot including InputPayload, so stamping a start time could erase a continuation marker another instance had just claimed. Guarded on StartedAt IS NULL so it is naturally once-only and safe to call on every pass.

      Parameters

      Returns Promise<boolean>

    • Replaces a task's input, guarded on the status the caller believes it is in.

      Why this is a guarded statement and not task.Save(). The obvious shape — load, check Status === 'Pending' in memory, save — is an unconditional full-row UPDATE carrying the whole loaded snapshot. A task claimed between the load and the save has its Status, ClaimedBy and ClaimExpiresAt reverted to that snapshot while its body executes, after which a second instance claims it again and the step runs twice. That is the stale-snapshot class this file's header exists to prevent, and it does not become safe because the window is small — the dispatcher polls every few seconds.

      expectedStatus is a parameter because two verbs need it: editing the brief of a step that has not started (Pending) and correcting the brief of one that failed, on the way into a retry (Failed).

      Parameters

      • provider: IMetadataProvider
      • taskID: string
      • inputPayload: string
      • expectedStatus: "Failed" | "Pending"
      • workflowTaskTypeID: string
      • contextUser: UserInfo

      Returns Promise<boolean>

    • Updates a graph parent's in-flight progress — column-scoped, and refused once it is terminal.

      The race this closes needs no exotic timing. Instance A loads the graph while a child is still In Progress and computes a non-terminal rollup. Instance B loads after that child finishes, settles the parent and claims the continuation. A's full-row progress Save() then lands: Status reverts to non-terminal and A's pre-marker InputPayload snapshot erases the marker. The next pass finds a non-terminal parent with a terminal rollup and an absent marker — so it settles again and delivers again. That is the duplicate reinvoke P4 exists to prevent, arriving through the last unguarded window.

      "These writes happen before settlement" is true per instance and false across instances, which is exactly the kind of timing argument a guard replaces with a structural one.

      Parameters

      Returns Promise<boolean>

    • Writes named fields of a graph's debug bag, leaving every other field alone.

      Why field-scoped rather than rewriting $.debug. A read-merge-write of the whole bag is the same stale-snapshot hazard as a full-row save, one level down: a verb that reads the bag, merges its own change, and writes the result puts back whatever the fields it does NOT own held at read time. The sharp case is the step allowance — if the dispatcher consumes it between a SetBreakpoints read and its write, the rewrite resurrects the consumed allowance and one press of Step releases two waves, straight through the CAS that exists to prevent exactly that. Writing only the paths a verb owns removes the class rather than narrowing the window.

      Paths are nested JSON_MODIFY calls, so the whole set lands in one statement.

      Parameters

      Returns Promise<boolean>