OptionalcontinuationDeliverer: TaskContinuationDelivererOptional. Absent means a host that cannot post messages or start agent turns — a worker, a test. The dispatcher still records and logs every completion, so a graph's outcome is never lost; it simply is not announced.
Optionalobserver: TaskGraphObserverOptional. Absent means nobody is watching — the dispatcher behaves identically, it just announces nothing.
OptionalactionRunner: TaskActionRunnerOptional. Absent means this host cannot run action nodes; they stay Pending and visible rather than being failed, because "nobody here can run this" is not "this ran and broke".
OptionalpromptRunner: TaskPromptRunnerOptional. Absent means this host cannot run prompt nodes; they stay Pending and visible rather than being failed, for the same reason action nodes do.
ReadonlyShutdownName shown in the shutdown drain log.
Run a pass now instead of waiting for the next poll tick.
Submit calls this (via KickTaskGraphDispatchers) so a just-written graph is claimed in milliseconds rather than up to TaskGraphDispatcherConfig.PollIntervalSeconds.
Reclaims expired claims and reports anomalies.
Also enforces the two schema promises that previously had no enforcer anywhere: agent tasks
left In Progress with no claim are surfaced loudly rather than silently corrected, since
that shape indicates tampering or a bug and Record Changes already carries the audit trail.
IShutdownable — idempotent by way of Stop's running guard.
Begins dispatching.
Runs reconciliation FIRST, before accepting any new work. On a restart this instance may be looking at tasks its own previous incarnation claimed and never released — reclaiming those up front is what turns a crash from "work stranded forever" into "work resumes".
Stops accepting new work and waits for everything already started to finish.
"Everything" includes the timer passes, and that is the fix. This waited only on
inFlight — the task executions — while a poll pass is a void-ed promise nothing held. So
Stop() returned while a pass was mid-flight, and that pass went on to settle graphs, emit
lifecycle frames and CLAIM NEW TASKS afterwards. Three consequences, all of them quiet:
GraphSettled frame arrived after every subscriber had gone, so the settlement was
invisible to exactly the viewer watching for it;Stop() resolved, and the still-running pass's
statements collided with it (Requests can only be made in the LoggedIn state).A pass is bookkeeping for work that already happened, so it is DRAINED rather than cancelled: abandoning one halfway is the crash window the unsettled sweep exists to rescue, and choosing to open it on every clean shutdown would be perverse.
Deliberately does NOT release claims on the way out: an abandoned claim expires on its own, and releasing eagerly would hand a still-running task to another instance mid-execution. Letting the TTL do it is the safer failure mode.
Implemented by long-lived singletons (engines, services) that own background work — timers, intervals, subscriptions, sockets, child processes — so a coordinated shutdown can release them. Without this contract,
SIGTERM/SIGINThandlers in the entry point have no way to reach into singleton state to clear timers, leaving the process unable to exit cleanly under load tests, CI runs, or graceful drains.Implementers register themselves with
ShutdownRegistry.Instance.Register(this)(typically in their constructor) and provide aShutdown()that releases the resource synchronously or returns a promise. The registry calls each implementer exactly once perShutdownAll()invocation.