-
Notifications
You must be signed in to change notification settings - Fork 3
Domains Dispatch
The dispatch domain owns the lifecycle of a worker run from DispatchRequest through admission, spawning, event-pumping, and receipt finalization. It resolves a DispatchRequest to a TargetDescriptor plus RuntimeDescriptor plus wire model id via the providers contract, gates admission on safety scopes, concurrency, budget, and capability flags, then spawns a native worker subprocess that reports back over a structured event stream. The domain is pure data plus orchestration: the worker process is the only place that speaks the Pi SDK protocol, and the dispatch bundle in the orchestrator process reads that stream, meters tokens, collects tool stats, and seals the receipt.
The domain manifest at src/domains/dispatch/manifest.ts declares dependencies on config, safety, agents, providers, middleware, prompts, and scheduling. Every other domain reaches dispatch through src/domains/dispatch/index.ts, which re-exports the public surface: DispatchDomainModule, createDispatchDomainModule, and the createDispatchBundle extension factory.
DispatchDomainModule is the DomainModule object that the core DomainLoader consumes during composition. It carries the DispatchManifest and a createExtension function that delegates to createDispatchBundle. The factory createDispatchDomainModule(options) wraps the extension in a closure that forwards the DispatchBundleOptions to createDispatchBundle at bundle-creation time.
The three composition roots that construct a dispatch bundle are:
-
src/entry/orchestrator.ts(interactive mode) -
src/cli/run.ts(headlessclio-coder run) -
src/cli/fleet.ts(headless fleet CLI)
The makeDispatchBundle helper in tests/harness/dispatch.ts mirrors the same construction for test bundles.
createDispatchBundle in src/domains/dispatch/extension.ts returns a DomainBundle<DispatchContract>. The contract object (visible from line 7602 onward in extension.ts) exposes:
-
dispatch(req: DispatchRequest)— the primary entry point. It validates the request, resolves the route, admits the run against capacity and budget, spawns a worker, pumps events, finalizes the receipt, and returns aRunReceiptviarun.finalPromise. -
dispatchBatch(batch: DispatchBatchRequest)— the parallel-batch path used by the fleet execution plan. -
preview(req)— a side-effect-free route preview used by speculative dispatch. -
planAgentSelection(input)— resolves the agent-selection decision for a route decision. -
routeCandidates(req)— enumerates the route candidates for a dispatch. -
reservations— a set of prepare/release/rollback operations for dispatch capacity reservations. -
speculate(prediction)/releaseSpeculative(reason)— manages held worker processes for speculative dispatch. -
listRuns(status)/getRun(runId)— reads from the in-memory ledger. -
observedRunWrites(runId)/observedRunWriteAttribution(runId)— returns the write-targets this process finalized. -
assignments— get/getStored/flushWrites for assignment records. -
drainMember(runId)— awaits all pending work for a run member. -
abort(runId, reason)— cancels a live or queued run.
The DispatchContract type is defined in src/domains/dispatch/contract.ts and is the typed interface every caller sees.
A call to dispatch(req) follows this path:
-
Validation.
validateJobSpecinsrc/domains/dispatch/validation.tsvalidates the raw spec. It rejects unknown keys, enforces type constraints, checks thatfailover: "approved"carriesallowedCandidates, validatespipelineInput,predecessorHandoffs,lineage,gate,plan,competeStance, and the dispatch-intent compatibility classification. On success it returns{ ok: true, spec: JobSpec }. -
Route resolution. The bundle calls
planActiveRouteorresolveJointRoute(viaactive-route-planner.ts/joint-route-resolver.ts) to resolve the target, runtime, and wire model id. The providers contract suppliesresolveRuntimeTargetandresolveEndpointCapacities. -
Admission.
admitinsrc/domains/dispatch/admission.tschecks global concurrency, per-endpoint capacity, budget, and safety scope. ThecreateCapacityAdmissionControllermanages slot leases. AforegroundEndpointBlocktest intests/contracts/dispatch-admission.test.tsdemonstrates the deadlock guard: a run that needs the same endpoint already held by a foreground stream is refused without spending the queue timeout. -
Worker spawn.
spawnNativeWorkerinsrc/domains/dispatch/worker-spawn.tscreates the subprocess. The worker spec (WorkerSpecfromsrc/worker/spec-contract.ts) carries the serialized runtime descriptor, prompt composition, tool surface, budget, and protected-artifact state. -
Event pump.
startDispatchEventPumpinsrc/domains/dispatch/event-pump.tsreads the worker's NDJSON stdout stream. The pump folds events into theRunEnvelope(token meters, tool stats, steering provenance, finish-contract entries) and collects tool-call statistics viarecordToolStart/recordToolFinishintool-stats.ts. -
Finalization.
resolveRunOutcomeinsrc/domains/dispatch/outcome.tsresolves the terminalRunOutcomefrom the worker's exit code, heartbeat status, and termination evidence. The receipt is sealed viawithReceiptIntegrityinsrc/domains/dispatch/receipt-integrity.ts, which computes the sha256 digest over the receipt's canonical JSON payload. -
Ledger write.
writeFleetRuninsrc/domains/dispatch/state.tsappends theRunEnvelopeto theruns.jsonledger and writes the sealedRunReceipttoreceipts/<runId>.json.
decideRoute in src/domains/dispatch/route-decision.ts is the pure function that selects a route. It operates on RouteDecisionInput and returns a RouteDecisionV1. The algorithm:
-
Hard filters. Candidates with a non-null
rejectionare excluded from the admissible set. The count of rejected candidates is recorded as areasonCode. -
Active readiness. In
activemode, candidates whoseactiveReadiness.readyis false are further excluded. -
Posture floors.
clearsPostureFloorsfilters candidates whose estimate does not satisfy the posture's quality/cost/latency floors. -
Scoring and ranking.
scoreRouteassigns a score to each admissible candidate.compareRankedRoutesorders them. For gate routes, a secondary sort breaks ties in favor of routes independent of the subject being graded (viagateRouteCorrelation). -
Dominated elimination.
dominatesRoutemarks candidates that are strictly dominated by another on all cost-quality-latency axes. -
Selection. In
manualposture the executed route is selected. Inshadowmode the best admissible candidate is recorded but never affects execution. Inactivemode the best candidate replaces the executed route.
The RouteCandidate interface names the whole operational tuple: agentId, specFingerprint, executionRole, targetId, modelId, runtimeId, nodeId, thinkingLevel, toolSignature, promptCompositionHash, endpointIdentityHash, and settingsFingerprint. The candidate key (routeCandidateKey) hashes these fields so two routes that differ in any one are distinct.
fixedRouteDecision is a failure-isolated fallback: when the route observer or its durable inputs fail, callers seal this exact one-candidate decision so a receipt never loses its routing evidence.
executeFleetRun in src/domains/dispatch/fleet-run.ts is the single fleet-run execution path. It drives a compiled ExecutionPlan through the ExecutionSchedulerAdapter and returns a FleetRunOutcome. The plan can contain agent steps, code steps, and loop constructs. The scheduler in src/domains/dispatch/execution-scheduler.ts (executePlan) handles wave-based parallelism, predecessor handoffs, and loop repair cycles.
planFleetResume in the same file produces a FleetResumePlan for a partially completed fleet run. It diffs the prior run record against the current plan and identifies which steps can be replayed from their sealed receipts versus which must be re-executed.
The ExecutionSchedulerAdapter interface exposes preflight, reserve, run, and optionally runCode. The orchestrator supplies the adapter's run as the dispatch bundle's dispatch method. runCode is the deterministic code-step path that takes no worker reservation.
src/domains/dispatch/gate-decisions.ts manages the durable evidence for review and compete gates. Worker receipts seal before a reviewer verdict or judge winner can be parsed, so the coordinator writes a separate integrity-covered GateDecisionArtifact that links the decider receipt to every subject receipt.
The lifecycle is:
-
Stage.
stagePendingGateOutputwrites aPendingGateOutputRecordas soon as the reviewer/judge event stream settles. At this point the decider receipt may still be finalizing, so the run id is durable but its receipt digest is not yet known. -
Resolve.
resolvePendingGateDecisionbinds the staged output to the now-sealed receipt and parsed outcome.decideReviewGateapplies the coordinator's review policy: a pass or terminal-cycle fail is the verdict; a non-terminal fail earns areviseoutcome with findings threaded to the next builder. -
Materialize.
materializePendingGateDecisionwrites the finalGateDecisionArtifacttogate-decisions/<id>.jsonand clears the pending WAL record.
verifyGateDecisionArtifact checks the semantic validity and sha256 integrity digest. readGateDecisionArtifacts and readGateDecisionArtifactsForRunIds discover trusted decisions from receipt identities.
parseCompeteGateResult validates a judge's answer: the winner must be an integer 1..candidateCount, and the result must carry typed checks with evidence.
validateJobSpec in src/domains/dispatch/validation.ts is a pure function that accepts unknown input and returns { ok: true, spec: JobSpec } or { ok: false, errors: string[] }. It enforces:
-
Known keys only. Any key not in the
KNOWN_KEYSset is rejected. -
Briefing bounds.
DISPATCH_BRIEFING_MAX_BYTESis 12,000 for external callers;INTERNAL_DISPATCH_BRIEFING_MAX_BYTESis 64 KiB forrequestOrigin: "internal"callers. -
Failover consistency.
failover: "approved"requires non-emptyallowedCandidates;failover: "automatic"is rejected on plan-approved dispatches. -
Write roots. When
writeRootsis present, it must be a non-empty array of non-empty strings. The validator resolves them against the job'scwd. -
Intent compatibility. The dispatch-intent migration rules are classified here via
classifyDispatchIntentCompatibility, so the compatibility table is true of every dispatch path (fleet, CLI, ACP, extension).
src/domains/dispatch/types.ts defines the shared run and receipt types:
-
RunStatus— the ledger row's status:queued,running,completed,failed,interrupted,stale, ordead. -
RunOutcome— the terminal outcome taxonomy:succeeded,failed,timed_out,stalled,canceled,denied_by_policy,spawn_failed.RETRYABLE_OUTCOMESis the set{failed, timed_out, stalled, spawn_failed}. -
RunLineage— proof-of-work lineage. Retries inheritrootRunIdand incrementattempt; nested dispatch incrementsdepthand resetsattempt. -
RunEnvelope— the live ledger record, kept inruns.json. -
RunReceipt— the per-run artifact sealed underreceipts/<runId>.json. -
RunGateProvenance— review/compete gate provenance sealed into the receipt. References point backward: a reviewer references the builder it reviewed, a revise builder references the reviewer whose findings it received. -
RunReceiptIntegrity— the receipt integrity version 20, withalgorithm: "sha256"and adigestfield. The version constant is annotated against this type, so bumping one without the other is a compile error.
tests/contracts/dispatch-admission.test.ts demonstrates the admission boundary: foregroundEndpointBlock refuses a same-endpoint foreground deadlock without spending the queue timeout, and ordinary worker saturation remains queueable.
tests/contracts/dispatch-routing-deterministic.test.ts verifies that dispatch routing never waits on a decision model. With a 2.5s-late decision endpoint bound to every DECISION_SITES, the worker still starts as it would unbound. The test measures the spawn delay and asserts it stays low.
tests/contracts/route-readiness-reads.test.ts proves that a dispatch reads only the receipts written since the previous readiness window. It patches fs.readFileSync to count reads, then asserts that the first readinessWindow() reads 13 files (runs.json + 12 receipts), and subsequent windows on an unchanged ledger cost zero file reads.
tests/extended/route-identity-keying.test.ts demonstrates that prompt-wording edits preserve the quality denominator (all six observations remain visible), that model/target/node/runtime/spec/role/thinking changes start a fresh bucket, and that behavior-changing drift (tool surface, endpoint) invalidates the bucket without sharding it.
-
DispatchBundleOptions— the options bag passed tocreateDispatchBundle. It controls worker spawning (spawnWorker), held-worker management (spawnHeldWorker), node resolution (resolveNode), heartbeat specs, resilience cooldowns, settings access, session identity, protected-artifact state, reproducibility collection, route observation, and event journaling. -
ExecutionSchedulerAdapter— the adapter interface thatexecutePlancalls to preflight, reserve, run, and optionally run code steps. The orchestrator implements it; tests provide stubs. -
RouteDecisionInput— the input todecideRoute. New routing policies are added by extending theposturevocabulary and thescoreRoute/clearsPostureFloorsfunctions inroute-policy.ts. -
JobSpec— the validated dispatch request. New fields are added to theKNOWN_KEYSset invalidation.tsand to theJobSpecinterface.
-
Receipt integrity digest. The
RunReceiptIntegrityversion is 20. Bumping it without updating thereceipt-integrity.tsconstant is a compile error, but adding a field toRunReceiptwithout accounting for it in the canonical JSON serialization changes every digest. ThewithReceiptIntegrityfunction must cover the new field. -
The
exactOptionalPropertyTypesflag is on. Pass optional fields with...(x !== undefined ? { x } : {}), neverx: undefined. -
Worker stdout is the NDJSON bulk lane. One stray
console.login worker code corrupts the protocol. CalldrainStdout()before exit. -
Gate decisions are append-only. A
GateDecisionArtifactis sealed with a sha256 digest. Mutating it invalidates the evidence chain. UsestagePendingGateDecision/materializePendingGateDecisionfor the atomic write. -
validateJobSpecis the single gate. Every dispatch path (fleet, CLI, ACP, extension) goes through it. Adding a field without adding it toKNOWN_KEYSbreaks every caller. The intent-compatibility classification is also classified here, so a change to the compatibility table requires updating this validator. -
The route decision hash is deterministic.
decisionHashcovers the inputs, not the wall clock. Do not add a timestamp or random value to the decision input. -
fixedRouteDecisionis the last-resort fallback. When the route observer fails, callers seal this one-candidate decision. Do not add new failure modes that skip it, or receipts will lack routing evidence.
Source and generation metadata
title: "Dispatch domain"
summary: "How Clio resolves, admits, executes, and finalizes worker runs: the dispatch bundle's contract, routing decisions, fleet execution, and gate-decision persistence."
sources:
- "src/domains/dispatch/index.ts"
- "src/domains/dispatch/extension.ts"
- "src/domains/dispatch/types.ts"
- "src/domains/dispatch/route-decision.ts"
- "src/domains/dispatch/fleet-run.ts"
- "src/domains/dispatch/execution-scheduler.ts"
- "src/domains/dispatch/gate-decisions.ts"
- "src/domains/dispatch/validation.ts"
- "src/domains/dispatch/manifest.ts"
symbols:
- "DispatchDomainModule"
- "createDispatchDomainModule"
- "createDispatchBundle"
- "decideRoute"
- "fixedRouteDecision"
- "RouteDecisionV1"
- "RouteCandidate"
- "executeFleetRun"
- "planFleetResume"
- "ExecutionSchedulerAdapter"
- "executePlan"
- "GateDecisionArtifact"
- "decideReviewGate"
- "parseCompeteGateResult"
- "validateJobSpec"
- "JobSpec"
- "RunEnvelope"
- "RunReceipt"
- "RunOutcome"
- "RunLineage"
- "RunGateProvenance"
tests:
- "tests/contracts/dispatch-admission.test.ts"
- "tests/contracts/dispatch-routing-deterministic.test.ts"
- "tests/contracts/route-readiness-reads.test.ts"
- "tests/extended/route-identity-keying.test.ts"
invariants:
- "Every admitted runtime kind enters through the native worker subprocess; the worker entry rehydrates the runtime descriptor and delegates behind the engine boundary."
- "Hard constraints eliminate: a candidate the admission chain rejected can never appear as selected or in approvedFallbacks at any posture."
- "The route decision is deterministic: decisionHash covers the inputs, not the wall clock, so equal inputs produce an equal hash and offline replay reproduces it."
- "Every run that reaches finalization gets exactly one outcome, resolved at the single finalization point in extension.ts via resolveRunOutcome()."
- "A dispatch reads only the receipts written since the previous readiness window, not the full ledger."
validate:
- "pnpm run ci"Clio Coder · Repository · Website · Documentation
Wiki v0.1 · Developing implementation reference · Source snapshot: 657dce13d. Authored architecture documents define the product contracts.
- Clio Coder GUI Client
- apps / clio-coder-gui
- Apps clio coder gui server
- Apps clio coder gui tests
- apps
- Architecture
- Command-line surfaces
- Core
- Domains agents
- Config Domain
- Context Domain
- Dispatch domain
- Domains evidence
- Domains extensions
- Domains gateway
- domains
- Domains interop
- Domains lifecycle
- Domains memory
- Middleware Domain
- Domains mux
- Domains observability
- Domains plugins
- Prompt Compiler
- Domains providers
- Domains quota
- Domains resources
- Domains safety
- Domains scheduling
- Domains session
- Vendored Tool Registry and Resolution
- Engine
- Engine acp
- Engine apis
- engine
- Entry point
- Interactive
- interactive
- Interactive overlays
- Interactive renderers
- clio-coder wiki
- Scripts
- Contract tests
- Tests extended
- tests
- Tools
- Tools data
- tools
- Tools verify
- Worker runtime