OptionalAgentRunID?: stringOptionalConfiguration?: stringThe 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?: stringOptionalOutputPayload?: stringtrue 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.
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.
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.
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.
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.
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.
how the settlement is being delivered, recorded alongside the marker so an expired settlement is distinguishable from a delivered one after the fact
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.
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.
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.
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.)
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
StaticDebugOne field of the debug bag, as a value the statement can write.
Typed rather than a raw SQL fragment so a caller cannot inject one: the shape decides how the value is rendered, and every string goes through escape.
Records a terminal outcome and releases the claim in one guarded statement.
Guarded on both
Status='In Progress'andClaimedBy=@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.