This is the full developer documentation for SuperDispatch # Which agent takes the card, and why. > When a SuperPipeline card is ready, SuperDispatch filters every agent, scores the ones left and names one. SuperPipeline holds the card for it for a moment. Every decision is a record. Early v0.0.1 runs in production with no board opted in yet. Placement is proven end to end on staging. [Status and limits](/status/) lists what it does not do yet. ## Four rules [Section titled “Four rules”](#four-rules) It advises; SuperPipeline holds SuperDispatch names a preferred agent. SuperPipeline holds the card for that agent for a short reservation that lapses on its own. SuperDispatch never claims a card or hands one over. It reads, and never edits a roster It reads cards, rosters, load and history through each product’s public API and owns none of their data. Roster and supply changes it finds are advice for a person. Nothing requires it Without it, every agent claims by pull as before. When it is slow or absent the cost is the reservation window, never the claim. Every decision is a record The card, every candidate, each filter’s result with its reason code, the score that ranked the rest, the hold, and what became of the card. Start with [what it is](/what-it-is/), then [how a placement works](/placement/overview/). To run it, see [Running it](/run/running-it/) and [Settings](/run/settings/). For agents, the same docs are published as plain text: [`/llms.txt`](/llms.txt) indexes them, and [`/llms-full.txt`](/llms-full.txt) is every page in one file. # Candidates and filters > Every filter a candidate passes through, in order, and the reason code each one rejects with. Every agent of the workspace goes through the filters for each card. A candidate that fails a filter is recorded with that filter’s **reason code** and is not scored. A candidate that passes them all is [scored](/placement/scoring/). An agent outside the [candidate set](/placement/overview/#the-candidate-set) goes through the first filter only. Every other candidate goes through all of them, so its record says each thing that was wrong, not just the first. ## The filters [Section titled “The filters”](#the-filters) In the order they run: | Filter | Rejects with | Passes when | | ------------- | -------------------- | --------------------------------------------------------------------------------------------------------- | | `roster` | `not-on-roster` | the agent is in the candidate set: on AgentPod’s roster for the board, or (with `all`) reporting its load | | `capability` | `capability-missing` | the agent has the capability the stage asks for (SuperPipeline’s `capability_mismatch`) | | `grant` | `grant-denied` | the card’s dispatch grant covers the agent (SuperPipeline’s `grant_denied`) | | `claimable` | `not-claimable` | SuperPipeline would let the agent claim the card, for every other reason it gives | | `isolation` | `isolation-too-weak` | the station is isolated at least as strongly as the stage asks | | `model` | `model-mismatch` | the station runs one of the models the stage asks for | | `tools` | `tool-missing` | the station has every tool the stage asks for | | `credentials` | `credential-missing` | the station reaches every credential name the stage asks for | | `policy` | `policy-denied` | the workspace’s dispatch policy lets the agent take this work | | `health` | `unhealthy` | the agent is healthy by its load: ready to take a card held for it | | `capacity` | `at-capacity` | the agent is under its board’s concurrency (SuperPipeline’s `concurrency_reached`) | | `idle` | `not-idle` | the agent has less in hand than its capacity anywhere in the workspace | | `held` | `holding-another` | the agent holds no other card now | | `noShow` | `recently-no-show` | the agent has not let two holds lapse unclaimed in the last 30 minutes | | `passedOver` | `passed-over` | the agent’s hold on this card has not already lapsed in the card’s current ladder | ## What each one means [Section titled “What each one means”](#what-each-one-means) **`not-on-roster`.** At first SuperDispatch places only agents on AgentPod’s roster for the board. An agent that claims with its own token is recorded as outside the set and keeps working by pull, as it did before. `SUPERDISPATCH_CANDIDATES=all` widens the set. **`capability-missing`, `grant-denied`, `at-capacity`, `not-claimable`.** SuperPipeline’s eligibility read judges every agent of the workspace for the card and gives its reasons. SuperDispatch names three of them with its own filters. Any other reason, including one a later SuperPipeline release adds, and an “ineligible” with no reason at all, is `not-claimable`. **The station filters** (`isolation-too-weak`, `model-mismatch`, `tool-missing`, `credential-missing`) run only for a stage that states what a station needs. Each rejection records what the station lacks. See [Station filters](/placement/station-filters/). **`policy-denied`** runs only with `SUPERDISPATCH_POLICY=on`, and only narrows. See [The dispatch policy](/placement/policy/). **`unhealthy` and `not-idle`** read the [load shape](/placement/overview/#the-load-shape). For a rostered agent, idle means no work in hand at all: capacity is 1. This is the workspace-wide cap the charter asks for, so a hold is only ever made for an idle agent. SuperPipeline’s per-board concurrency (`at-capacity`) stays as it is. **`holding-another`.** One live hold per agent. An agent claims one card at a time, so a second hold could only lapse against it. **`recently-no-show`.** Each hold that lapses unclaimed is a no-show for its agent. Two within 30 minutes, on any cards, shut the agent out of every tier until the older one ages out. On a card open to any match it can still claim by pull. **`passed-over`.** Once an agent’s hold on a card lapses, the card’s ladder moves on without it. A new ladder starts when the card reaches another stage, comes back after a claim, or a person moves it. ## Failing only for load [Section titled “Failing only for load”](#failing-only-for-load) A candidate that fails only for its load (`at-capacity`, `not-idle`, `holding-another`, or `unhealthy` while it has no room) will pass once its work in hand is done. When nobody is eligible and such a candidate exists, the card waits for the next pass (`waiting-on-load` or `waiting-on-holds`) and nothing is recorded. See [When nobody is chosen](/placement/overview/#when-nobody-is-chosen). # Holds, tiers and fairness > How a hold works, how it relaxes from the winner to runner-ups to any match, and how one sweep shares free agents across boards. ## A hold [Section titled “A hold”](#a-hold) SuperDispatch never claims a card. It asks SuperPipeline to **hold** the card for one agent (`PUT /v1/boards/:id/cards/:cardId/reservation`). SuperPipeline checks the hold when a claim is made: * the held agent may claim the card; * any other agent’s claim waits until the hold lapses; * the hold lapses on its own, within its tier’s window (300 seconds at most), and the card is open to any matching agent again. A **reservation outranks priority**: the held agent gets its held card even when a higher-priority card with no hold is waiting. SuperDispatch already weighed priority when it chose. A SuperPipeline without the reservation verb refuses each hold. The decision is recorded with the outcome `not-reserved`, and nothing else changes. ## Tiers [Section titled “Tiers”](#tiers) SuperPipeline’s hold names one agent, so SuperDispatch relaxes it as a **ladder** of holds, one decision per step, each labelled with its tier and step: | Step | Tier | Held for | Window | | ------- | ----------- | -------------------------------------------------- | ---------------------------------------- | | 1 | `winner` | the best candidate | `SUPERDISPATCH_RESERVATION_SECONDS` (60) | | 2 to k | `runner-up` | the best candidate whose hold has not lapsed on it | `SUPERDISPATCH_RUNNER_UP_SECONDS` (30) | | after k | `any-match` | nobody: pull decides, as without SuperDispatch | until claimed or moved | k is `SUPERDISPATCH_TOP_K` (3). With k = 1, the winner is held and then any match. * **The next step is taken on the first pass after a hold lapses**, so between two steps any matching agent may claim by pull. * **A runner-up step with nobody left to hold for** opens the card to any match at once. * **The winner step with nobody eligible** is `none-eligible`, and the card is tried again in five minutes. * **A card open to any match is not held again** until a new ladder starts: when the card reaches another stage, comes back after a claim, or a person moves it. * **Each lapse is a no-show** for its agent. Two within 30 minutes, on any cards, shut the agent out of every tier until the older one ages out (`recently-no-show`); on an open card it can still claim by pull. * **A winner that claims late**, after its hold lapsed but before the next pass, is a claim, not a no-show. ## Fairness across boards [Section titled “Fairness across boards”](#fairness-across-boards) A busy board does not starve a quiet one. `SUPERDISPATCH_FAIRNESS` says how one sweep orders every board’s ready cards: * **`round-robin`** (the default): one card from each board a round, and within a round the card that has waited longest goes first. A busy board and a quiet one get a card each in the first round, and the busy board takes the free agents left after that. * **`board-order`**: every card of one board before the next: no fairness. ## The placement window [Section titled “The placement window”](#the-placement-window) A sweep every 30 seconds would lose most cards to AgentPod’s bridge, which claims a ready card within seconds. The [placement window](/placement/overview/#the-placement-window) closes that gap: SuperPipeline holds a newly ready card back from pull for a few seconds and tells SuperDispatch, which places it at once. ## The cost [Section titled “The cost”](#the-cost) The charter names it: another idle agent may wait up to the window before it can take a held card. A slow or absent SuperDispatch costs only that window, never the claim. If SuperDispatch stops, every hold it made lapses and every card is claimed by pull as before. # A placement, end to end > One pass over a board, from reading the cards to recording the decision, and the load shape every candidate is judged by. SuperDispatch places cards on the boards the operator [opted in](/run/opting-a-board-in/). It looks at every one of them on a sweep, every `SUPERDISPATCH_SWEEP_SECONDS` (30 by default), and at one board as soon as the event relay delivers one of its events. ## One pass [Section titled “One pass”](#one-pass) 1. **Settle what it held.** It reads each board from SuperPipeline and settles what became of each card it holds: | Outcome | What happened | | ------------------ | ----------------------------------------------------------------- | | `claimed` | the chosen agent claimed the card | | `claimed-by-other` | somebody else claimed it: another agent, or one off the roster | | `expired` | the hold lapsed and the card still waits | | `superseded` | a person moved or archived the card before anyone claimed it | | `not-reserved` | SuperPipeline refused the hold, so nothing was held (set at once) | A winner that claims late, after its hold lapsed but before the next pass, is `claimed`, not a no-show. 2. **Queue the ready cards.** A card is ready when it waits in a capability lane and is not already held. Each board’s cards are queued, and the boards are taken [fairly](/placement/holds-and-tiers/#fairness-across-boards): one card from each board a round, the longest-waiting card first within a round. 3. **Place each card.** For a card, SuperDispatch reads the board’s roster from AgentPod and the card’s eligibility from SuperPipeline (which judges every agent of the workspace), then: * [filters](/placement/filters/) every agent, each rejection with a reason code; * [scores](/placement/scoring/) the ones that passed every filter; * with two or more rostered candidates, chooses the best and asks SuperPipeline to [hold the card](/placement/holds-and-tiers/) for it for the tier’s window. With one, there is no choice to make and pull already gives it the card (`one-candidate`). 4. **Record the decision**: the card, the policy, the tier and step, every candidate with each filter’s result and the score that ranked it, the hold and, on a later pass, the outcome. See [Decision records](/records/decisions/). After the sweep it computes [roster suggestions](/records/roster-suggestions/) and [supply proposals](/records/supply-proposals/) from its own decisions. ## When nobody is chosen [Section titled “When nobody is chosen”](#when-nobody-is-chosen) A decision that chooses nobody says why: | Reason | When | Recorded | | ------------------ | --------------------------------------------------------------------------------------- | -------- | | `no-candidates` | nobody was considered | yes | | `one-candidate` | fewer than two candidates are on the roster: no choice to make, pull decides | yes | | `none-eligible` | at the winner step, every candidate failed a filter | yes | | `any-match` | the tiers are spent, or a runner-up step has nobody left: nothing is held, pull decides | yes | | `waiting-on-holds` | nobody is eligible, but a free agent fails only because it holds another card | no | | `waiting-on-load` | nobody is eligible, but an agent fails only for its load: work in hand, or busy | no | The two waiting reasons are not recorded. The card is looked at again on the next pass, so it is held for the first agent to free up, rather than recorded as `none-eligible` and looked away from while that agent pulls it unplaced. A card whose last decision chose nobody, or whose hold was refused, is not decided again for five minutes, so a card nobody can take is not recorded every sweep. ## The candidate set [Section titled “The candidate set”](#the-candidate-set) `SUPERDISPATCH_CANDIDATES` says who is considered: * **`roster`** (the default): the agents on AgentPod’s roster for the board, an enabled bridge row each. An agent that claims with its own token is recorded as `not-on-roster` and keeps working by pull. * **`all`**: those, and every agent of the workspace with no bridge row on the board, once SuperPipeline reports its load and health (`GET /v1/agents/load`, read once a pass). When that read fails, only the roster is placed that pass. An agent with a disabled bridge row stays out either way: a person turned it off on this board. ## The load shape [Section titled “The load shape”](#the-load-shape) The health and idle filters read one shape, whichever source gave it: ```plaintext { agentId, inFlight, capacity, healthy, lastSeenAt, source: "roster" | "direct" } ``` | Source | `inFlight` | `capacity` | `healthy` | `lastSeenAt` | | -------- | ---------------------------------- | ------------------- | ---------------------------------------- | ------------------------------- | | `roster` | 1 when the station is busy, else 0 | 1 | the row is enabled and the bridge ready | when the station last took work | | `direct` | cards held over every board | as reported, else 1 | heard from on a run within the last hour | its latest run heartbeat | Idle is `inFlight < capacity`. A direct agent never heard from is not healthy: nothing says it is there, so it keeps pulling until it has run once. Every candidate record carries its `source`. ## The placement window [Section titled “The placement window”](#the-placement-window) AgentPod’s bridge claims a ready card within seconds, so a sweep alone would lose most cards to pull. When the operator opts a board in, SuperDispatch sets the board’s **placement window** on SuperPipeline (`SUPERDISPATCH_PLACEMENT_WINDOW_SECONDS`, 10 by default; 0 on opt-out). Inside it: * SuperPipeline holds a card that just became ready back from pull and relays `card.ready`; * SuperDispatch places that card on delivery, alone, with the roster the sweep cached (AgentPod’s roster read probes every station and takes seconds, longer than the window can spare); * a hold made inside the window is claimable by its agent at once. Any other event from a board makes SuperDispatch look at that board two seconds later. If SuperDispatch is absent, the window simply ends and pull proceeds. # The dispatch policy > How SuperDispatch asks Accounts' dispatch policy who may take a card, how it fails closed, and why it is still advice. A workspace can keep a **dispatch policy** in Accounts: rules about which agents may take which work. With `SUPERDISPATCH_POLICY=on`, SuperDispatch asks it about each candidate and records the answer. `off`, the default, asks nothing, and the `policy` filter passes everyone. ## It only narrows [Section titled “It only narrows”](#it-only-narrows) An agent SuperPipeline already refuses on the card’s grant is never asked about, and a policy never adds an agent the grant leaves out. The policy can only take candidates away. ## How it is asked [Section titled “How it is asked”](#how-it-is-asked) * **`POST /api/policy/decide`** on Accounts, which needs the scope `policy:decide` on this service’s credential. * **One question per agent, board stage, labels and capabilities a pass**, at most. SuperPipeline cards carry no labels yet, so none are sent; the capabilities are the stage’s `requires`. * **The answer is the verdict.** It is recorded on the candidate as `policy: { allow, reasons, policyVersion }`. A workspace with no policy answers `no-policy`, which allows. ## Limits [Section titled “Limits”](#limits) A policy may set a limit: an agent that already had `maxRuns` holds from SuperDispatch within the last `windowHours` is rejected, with the reason `limit:`. It counts holds SuperDispatch made, so it limits what is steered to an agent, not what the agent pulls. ## When Accounts cannot answer [Section titled “When Accounts cannot answer”](#when-accounts-cannot-answer) Accounts unreachable, a refusal, or an answer that cannot be read: * if the last answer said the workspace has **no policy**, the candidate passes; * otherwise, including a cold start, it is rejected with the reason `policy-unread`. That fails closed, and costs only the advice: a card nobody is held for is still claimed by pull. An agent with no principal cannot be asked about, so it is treated the same way (`no-principal`). ## It is advice until SuperPipeline enforces it [Section titled “It is advice until SuperPipeline enforces it”](#it-is-advice-until-superpipeline-enforces-it) A pull ignores the policy: SuperPipeline does not check it on claim yet. With the policy on, SuperDispatch will not hold a card for an agent the policy refuses, but that agent can still pull the card. Until SuperPipeline enforces it, the policy here is advice like the rest of SuperDispatch. # Scoring > How the candidates that pass every filter are ranked - their record at this stage, their success on this board, then least recently used. Every candidate that passes all the [filters](/placement/filters/) is scored. The ranking is a fixed policy, recorded on each decision by name: `idle-stage-success-lru@3`. ## The order [Section titled “The order”](#the-order) Candidates are compared term by term; a later term only breaks a tie in the earlier ones. 1. **`stageRecord`**: the agent’s record at this board stage, from SuperWitness. Higher first. 2. **`successRate`**: its success on this board, from SuperDispatch’s own holds. Higher first. 3. **`sinceLastDispatchS`**: seconds since it last took work. Longer first: least recently used. 4. The agent id, so the order is always the same for the same facts. The decision records each candidate’s `score` as `{ total, terms }`. `total` is how many eligible candidates it outranks plus one, so the best has the highest total and the record reads as a ranking. `terms` holds the numbers above, so the record says what weighed. ## Success on this board [Section titled “Success on this board”](#success-on-this-board) `successRate` is the share of SuperDispatch’s settled holds for the agent on this board that it claimed, smoothed: `(claimed + 1) / (settled + 2)`. A new agent starts at one half. ## Least recently used [Section titled “Least recently used”](#least-recently-used) `sinceLastDispatchS` reads `lastSeenAt` from the [load shape](/placement/overview/#the-load-shape): when the station last took work, or a direct agent’s latest run heartbeat. A station that never took work ranks first among the least recently used. ## History at the stage [Section titled “History at the stage”](#history-at-the-stage) When `SUPERWITNESS_URL` is set, each pass reads SuperWitness’s finished runs at each board stage it places a card at, once per board stage: ```plaintext GET /v1/runs?source=superpipeline&scope=&stage_key=&status=succeeded&status=failed&since= ``` up to 1000 runs from the last `SUPERDISPATCH_HISTORY_DAYS` (30). They are joined to candidates by the run’s executor principal. A cancelled run judged nothing and is not read. For each candidate: * each run weighs `0.5 ^ (age / half-life)`, with a half-life of `SUPERDISPATCH_HISTORY_HALF_LIFE_DAYS` (7); * the **outcome rate** is the weighted share that succeeded, `(Σ w·succeeded + 1) / (Σ w + 2)`; * the **verdict rate** is the weighted mean of the newest verdict’s pass mark, `(Σ w·mark + 1) / (Σ w + 2)`: a `score` as it stands, a `decision` of pass, approve or accept as 1, and fail, reject or request_changes as 0; any other verdict is no mark; * each rate is exactly one half below `SUPERDISPATCH_HISTORY_MIN_SAMPLE` runs or marks (3), so a new agent is neither starved nor favoured; * the **stage record** is the mean of the two rates. Every candidate’s history is recorded on the decision as `stageHistory`: runs, succeeded, verdicts, passes, weight, both rates and the record. **History never blocks a placement.** SuperWitness unset, refusing or unreachable: the record term is left out for everyone, the ranking falls back to the board terms, and the candidates carry `stageHistory: null`. The record weighs only when it was read for every eligible candidate. Runs carry `stage_key` only from a SuperPipeline that reports it (its `SUPERWITNESS_REPORT_STAGE` setting). ## A learned model [Section titled “A learned model”](#a-learned-model) Learned routing can, past a gate, hold a model’s choice instead of the formula’s. It is off by default and records its choice beside the formula’s in shadow first. See [Learned routing](/records/learned-routing/). # Station filters > How a stage's placement (isolation, models, tools, credentials) filters candidates against AgentPod's capability registry. A SuperPipeline stage may say what a **station** needs to work it, in its `placement`. SuperDispatch checks each candidate’s station against AgentPod’s capability registry. AgentPod owns the catalog and the registry (charter decision of 2026-11-13); SuperDispatch only reads it. ## What a stage can ask for [Section titled “What a stage can ask for”](#what-a-stage-can-ask-for) | Need | Asks for | Rejects with | | ------------- | -------------------------------------------------------- | -------------------- | | `isolation` | the weakest acceptable: `host` < `container` < `sandbox` | `isolation-too-weak` | | `models` | any one of these models | `model-mismatch` | | `tools` | every one of these, in the ids of AgentPod’s catalog | `tool-missing` | | `credentials` | every one of these credential names | `credential-missing` | Each rejection is recorded on its candidate with `missing`: what the station lacks, in the stage’s words. A candidate whose station the registry does not list, or a candidate with no station, has nothing, so it fails each stated need. ## How strong a fact must be [Section titled “How strong a fact must be”](#how-strong-a-fact-must-be) Each fact in the registry carries a proof: `probed` (strongest), `installed` or `declared`. A fact counts only when its proof is at least `SUPERDISPATCH_MIN_PROOF`. The default, `declared`, counts every fact. With `installed`, a tool someone merely declared does not satisfy a need. ## When they run [Section titled “When they run”](#when-they-run) * **Off for a stage that states nothing.** No filter runs and nothing is recorded for them: such a stage is placed exactly as it was before station filters existed. * **On for a stage that states a placement.** A pass reads AgentPod’s registry once (`GET /api/catalog/stations`) and filters each candidate’s station. * **Unchecked when the registry cannot be read.** An AgentPod without the registry answers 404, or the read is refused: the pass leaves the needs unchecked, says so in the log, and places as if none were stated. * **Never, with `SUPERDISPATCH_STATION_FILTERS=off`.** The registry is not read at all. The hub answers the registry only to a service listed in its `CATALOG_READERS`. See [Running it](/run/running-it/#what-it-needs-from-the-others). ## What it leads to [Section titled “What it leads to”](#what-it-leads-to) When a lane goes unplaced because no station has what its stage asks for, the same records feed [supply proposals](/records/supply-proposals/): change a station’s model, move the work to a station that has what is missing, or set up a new one. SuperDispatch changes no station. # Decision records > What a placement decision records, every field of it, and how to read one to answer "why did this card go there?". Every placement is a **decision**, kept in SuperDispatch’s own store. A decision alone answers “why did this card go there?”: the card, the policy, the tier, every candidate with each filter’s result and the score that ranked it, the hold, and on a later pass what became of the card. A settled decision is also a labelled example for [learned routing](/records/learned-routing/). Decisions are read on the [operator API](/run/operator-api/): ```sh curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/decisions?board=brd_…&card=card_…&limit=20" curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/decisions/pld_… ``` The list is newest first. It takes `board`, `card` (with its board), `before` (a time) and `limit` (1 to 200, 50 when unset). ## An example [Section titled “An example”](#an-example) The field names are real; the agents and numbers are made up. ```jsonc { "id": "pld_3f9a0c1e7b2d4a6f8e0c1b2a", "boardId": "brd_review", "cardId": "card_1042", "stageKey": "review", "policy": "idle-stage-success-lru@3", "chosen": "agt_ada", "reason": null, "tier": { "name": "winner", "step": 1 }, "requires": { "all": ["code-review"], "any": [] }, "candidates": [ { "candidate": { "agentId": "agt_ada", "principalId": "prn_ada", "stationId": "stn_1", "source": "roster" }, "filters": [{ "filter": "roster", "passed": true }, { "filter": "capability", "passed": true } /* …the rest */], "score": { "total": 2, "terms": { "stageRecord": 0.81, "successRate": 0.75, "sinceLastDispatchS": 5400 } }, "stageHistory": { "runs": 12, "succeeded": 10, "verdicts": 6, "passes": 5, "weight": 7.4, "outcomeRate": 0.82, "verdictRate": 0.79, "stageRecord": 0.81 } }, { "candidate": { "agentId": "agt_cyd", "principalId": "prn_cyd", "stationId": "stn_3", "source": "roster" }, "filters": [{ "filter": "roster", "passed": true }, /* … */ { "filter": "idle", "passed": false, "reason": "not-idle" } /* …the rest */], "score": null, "stageHistory": null }, { "candidate": { "agentId": "agt_eli", "principalId": "prn_eli", "stationId": null, "source": "direct" }, "filters": [{ "filter": "roster", "passed": false, "reason": "not-on-roster" }], "score": null, "capable": true, "stageHistory": null } ], "reservation": { "reservedUntil": "2026-11-13T10:04:12.000Z" }, "reservationRefused": null, "outcome": { "status": "claimed", "by": "agt_ada", "at": "2026-11-13T10:03:31.000Z" }, "createdAt": "2026-11-13T10:03:12.000Z" } ``` ## The fields [Section titled “The fields”](#the-fields) | Field | What it is | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | | `id` | `pld_` and 24 hex digits | | `boardId`, `cardId` | the card, on its board | | `stageKey` | the stage the card was at | | `policy` | the ranking it was placed by: `idle-stage-success-lru@3`, or with `+learned:v` when a model placed it | | `chosen` | the agent held for, or null | | `reason` | why nobody was chosen, when nobody was ([the reasons](/placement/overview/#when-nobody-is-chosen)) | | `tier` | `{ name, step }`: `winner`, `runner-up` or `any-match`, and the ladder’s step; null on decisions made before tiers | | `requires` | the capability the stage asked for, `{ all, any }`; null on decisions made before roster suggestions | | `candidates` | every agent considered, below | | `reservation` | `{ reservedUntil }` when SuperPipeline held the card, else null | | `reservationRefused` | why SuperPipeline refused the hold, when it did | | `outcome` | `{ status, by, at }` once settled, else null | | `createdAt` | when it was decided | Each candidate: | Field | What it is | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `candidate` | `{ agentId, principalId, stationId, source }`: `source` is `roster` or `direct` | | `filters` | `[{ filter, passed, reason?, missing? }]`: every filter it went through; a rejection carries its reason code, and a station filter’s what the station lacks | | `score` | `{ total, terms }` when it passed every filter, else null ([Scoring](/placement/scoring/)) | | `capable` | only for an agent off the roster: whether it has the stage’s capability, by SuperPipeline’s verdict | | `stageHistory` | its record at this stage from SuperWitness, or null when that was not read | | `policy` | `{ allow, reasons, policyVersion }`, when the dispatch policy was asked | ## The outcome [Section titled “The outcome”](#the-outcome) | `status` | What became of the card | | ------------------ | ------------------------------------------------------------------------------------- | | `claimed` | the chosen agent claimed it | | `claimed-by-other` | somebody else claimed it: another agent, after the hold lapsed, or one off the roster | | `expired` | the hold lapsed and the card still waits | | `superseded` | the card left the stage or the board without a claim: a person moved or archived it | | `not-reserved` | SuperPipeline refused the hold, so nothing was held | ## Reading one [Section titled “Reading one”](#reading-one) * **Why this agent?** Find the candidate with the highest `score.total`. Its `terms` say what weighed: its record at the stage, its success on the board, how long since it last took work. * **Why not that agent?** Find its candidate. The first `passed: false` filter’s `reason` says what turned it away, and every other failing filter is there too. A station filter’s `missing` names what its station lacked. * **Why nobody?** `chosen` is null and `reason` says why. `one-candidate` and `any-match` are not failures: pull decides those cards. * **Did it work?** `outcome`. A run of `expired` outcomes for one agent is what shuts it out as a `recently-no-show`, and what the `unreliable` [roster suggestion](/records/roster-suggestions/) reads. * **Several decisions for one card** are the steps of its ladder: filter the list by `card` and read `tier.step`. ## Not yet an event [Section titled “Not yet an event”](#not-yet-an-event) The charter asks for each decision to be published through treaty’s relay, so SuperWitness can audit it and chat can show it. That needs a decision event in treaty’s catalogue first. Until then the operator API is the only way to read decisions. # Learned routing > The dataset of settled decisions, the model trained on it, and the shadow mode and gate it must pass before it places a card. Off by default. Off by default, and **it needs data first.** Every settled decision is a labelled example: the candidates, what each filter said, the score terms, the tier and what became of the card. That log, more than any formula fitted to it, is the asset, and it only grows as boards are placed. A model fitted on a few dozen decisions is noise: run it in shadow first. ## The dataset [Section titled “The dataset”](#the-dataset) `GET /v1/dataset?since=&format=jsonl|json` (schema 1): one row per settled decision and candidate, oldest first, at most 100,000 decisions a read. ```sh curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/dataset?since=2026-11-01" > dataset.jsonl ``` A row carries: * the decision: `decisionId`, `boardId`, `cardId`, `stageKey`, `capability`, `policy`, `tier`, `tierStep`, `createdAt`, `hour` (UTC), `outcome`, `outcomeBy`, `outcomeAt`; * the candidate: `agentId`, `source`, `chosen`, `eligible`, `filters` as `{ filter: true | reason }`, `score`, `stageHistory`; * how busy the pool was: `pool: { candidates, inSet, eligible, busy }`; * its `label`, its `target`, and the model’s inputs (`features`, null when it was not eligible). | Label | When | Target | | ---------------------- | -------------------------------------------------------------------------- | ------ | | `won-claimed` | chosen, and it claimed the card | 1 | | `won-expired` | chosen, and its hold lapsed unclaimed | 0 | | `won-claimed-by-other` | chosen, and somebody else claimed the card | 0 | | `lost-claimed` | not chosen (or nobody was, in the any-match tier), and it claimed the card | 1 | | `lost` | not chosen, and did not claim it | null | | `void` | a person moved the card, or the hold was refused: no signal about anyone | null | A `lost` candidate was never offered the card, so it says nothing about whether it would have taken it: it is kept, and not trained on. Only eligible candidates are trained on. The target is the claim: whether the claimed run then succeeded is SuperWitness’s to say, and is not joined yet. ## The model [Section titled “The model”](#the-model) `POST /v1/models/train`, body `{ since?, seed? }`: logistic regression over 12 features (stage record, whether it weighed, stage runs, board success rate, recency, never dispatched, direct, tier step, hour of day as sine and cosine, eligible candidates, pool busy), standardised, fitted by full-batch gradient descent from zero weights in plain TypeScript, with no dependency. * The holdout is a hash of each decision’s id with the seed (20%), so the same rows and seed give the same model. * The answer carries the metrics: `count`, `trainCount`, `holdoutCount`, `positives`, holdout `auc`, `accuracy`, `logLoss`, a five-bin `calibration` table, and `trainAuc` beside the holdout’s. * Each model is stored as the next version (`GET /v1/models`, `GET /v1/models/:id`). * Training only stores a model. It places nothing. ```sh curl -X POST -H "authorization: Bearer $TOKEN" -d '{"seed":1}' http://127.0.0.1:8796/v1/models/train ``` ## Shadow first [Section titled “Shadow first”](#shadow-first) `SUPERDISPATCH_LEARNED`: * **`off`** (the default): no model is read. Placement is the formula’s alone. * **`shadow`**: each placed decision also records what the latest model would have chosen and the probability it gave each eligible candidate. The formula’s choice is what is recorded and held: the model never changes a decision. * **`on`**: the model’s choice is held only when the model passes the gate: * trained on at least `SUPERDISPATCH_LEARNED_MIN_ROWS` rows (500); * at least `SUPERDISPATCH_LEARNED_MIN_SHADOW` settled shadow decisions (100); * in them, it beat the formula by at least `SUPERDISPATCH_LEARNED_MARGIN_PCT` points (5). Such a decision is recorded under the policy `idle-stage-success-lru@3+learned:v`. Short of the gate the formula places, as in shadow, and the log says which test failed. The gate is read again at most every minute. ## The shadow report [Section titled “The shadow report”](#the-shadow-report) `GET /v1/models/:id/shadow-report` compares the two choices against what happened. * Where they agree, both had the card’s outcome. * Where they disagree, only the formula’s choice was held, so the model is credited only when its choice claimed the card anyway (after the hold lapsed, or in the any-match tier). Otherwise what it would have done is unknown and counts as a miss, so the model’s rate is a lower bound. * Only decisions in which the formula’s choice had its chance (`claimed`, `claimed-by-other`, `expired`) count, and decisions the model placed itself are never its own evidence. The report also says how the gate reads under the current settings. ```sh curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/models/mdl_…/shadow-report ``` Placement stays pure: the loop reads the model from the store and hands it to placement as data. # Roster suggestions > How SuperDispatch flags lanes that rest on one agent, and what it suggests a person change. Advice only - it never edits a roster. A board where each capability has one agent stalls when that agent does. After each sweep, SuperDispatch reads its own recent decisions and flags the **lanes** (a board’s stage and the capability it asks for) that rest on too few working agents. Each flag is a **suggestion** for a person. **It never edits a roster.** A person makes the change, in AgentPod. The computation reads and writes SuperDispatch’s own store and nothing else: no client it holds of another product has a roster write, a test calls every method of every client and checks each request, and another fails if the computation touches a client at all. ## When a lane is flagged [Section titled “When a lane is flagged”](#when-a-lane-is-flagged) It reads the decisions of the last `SUPERDISPATCH_SUGGEST_WINDOW_HOURS` (24) on the opted-in boards, after each sweep or at most every `SUPERDISPATCH_SUGGEST_EVERY_SECONDS` (0: every sweep). | Rule | When | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | `one-candidate` | the lane’s latest decision found exactly one agent on the board’s roster with the capability | | `unreliable` | in three or more decisions, every agent on the roster with the capability was `unhealthy` or a `recently-no-show` | | `stalled` | a card waited at the stage, on one attempt, with nobody eligible (`none-eligible`) for `SUPERDISPATCH_STALL_MINUTES` (30) | ## What it suggests [Section titled “What it suggests”](#what-it-suggests) A flagged lane is one suggestion, of one of two kinds: * **`add-agent-to-board`**: the workspace has agents with the capability that are not on the board’s roster. They are the suggestion’s `candidates`. SuperPipeline’s eligibility read judges every agent of the workspace, and each decision records whether an agent off the roster has the capability (`capable`), so the decisions alone say who could be added. * **`lane-single-point`**: nobody else in the workspace has the capability, so somebody must set up another agent for it. ## What one reads [Section titled “What one reads”](#what-one-reads) ```plaintext { id, kind, boardId, stageKey, capability: { all, any } | null, rostered: [agentId], candidates: [{ agentId, principalId, stationId }], reasons: [rule], reason, evidence: [decision id], evidenceCount, firstSeenAt, lastSeenAt, resolvedAt, dismissedAt } ``` `reasons` are the rules that flagged it, `evidence` the decisions that bear it out. ## Its life [Section titled “Its life”](#its-life) * There is **one suggestion per kind, board and stage**. * A computation that **no longer finds it resolves it** (`resolvedAt`). * The operator can **dismiss** it (`POST /v1/suggestions/:id/dismiss`). It stays dismissed while it is found again. * One that resolved and **comes back later opens anew**. ```sh curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/suggestions?board=brd_…&state=open" curl -X POST -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/suggestions/sug_…/dismiss ``` ## Not in chat yet [Section titled “Not in chat yet”](#not-in-chat-yet) Suggestions are read on the [operator API](/run/operator-api/). Nothing posts them to a board’s room yet. # Supply proposals > What SuperDispatch proposes when a lane goes unplaced for want of agents or stations - a model, a station, a move or more capacity. Advice only - it changes no station. Placement can only choose among the agents and stations there are. When a lane goes unplaced for want of supply, the same computation that makes [roster suggestions](/records/roster-suggestions/) proposes what a person could change. **It never changes a station.** Each proposal names the verb a person would use (`howToApply`), in AgentPod or in SuperPipeline. SuperDispatch calls none of them and files no request for one: every one of those verbs is a person’s. A test calls every method of every client and fails on any request to such a path, or on any write but the reservation. ## What it reads [Section titled “What it reads”](#what-it-reads) The same decisions: each rejected candidate records which filter turned it away, and a [station filter](/placement/station-filters/) what the station lacked. And AgentPod’s capability registry, as it last answered. A lane gets proposals only **while its latest decision chose nobody**, and each kind needs **at least two decisions** in the window that bear it out. A **blocked** decision is one in which nobody was eligible: `none-eligible`, or `one-candidate` (a lane with one agent on the roster is left to pull, but its filters still ran). ## The kinds [Section titled “The kinds”](#the-kinds) | Kind | When | Target | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `change-model` | in blocked decisions, an agent on the roster was turned away only by `model-mismatch` | the agent, its station, the first model the stage asks for | | `move-to-host` | in blocked decisions, an agent on the roster was turned away only by station filters, and another station in the registry has all of what the lane’s stations lacked (an empty station first) | that station, its occupant, runtime, harness, isolation, model | | `add-station` | the same shortfall with no station in the registry that has it (or no registry read), or no agent on the roster has the capability and no agent in the workspace does | the isolation, models, tools, credentials or capability needed | | `raise-capacity` | in decisions that chose nobody, every agent on the roster with the capability was turned away for its load alone, at least one by `at-capacity` | none: the board’s agent concurrency in SuperPipeline | AgentPod’s registry names no host, so `move-to-host` names the station that has what is needed, and the runtime its node was provisioned on. When the registry says one of the lane’s own stations has what it lacked by now, nothing is proposed: the next decision will say. A registry fact counts only at `SUPERDISPATCH_MIN_PROOF` or stronger. ## How each would be applied [Section titled “How each would be applied”](#how-each-would-be-applied) | Kind | What a person does | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `add-station` | sets up a station in AgentPod (a runtime, then station setup) with what the target names, and adds its agent to the board’s roster | | `change-model` | declares the model for the station in AgentPod’s declared harness configuration | | `move-to-host` | puts the lane’s agent in the target station in AgentPod, or adds the target station’s agent to the board’s roster | | `raise-capacity` | raises the board’s agent concurrency in SuperPipeline | ## What one reads [Section titled “What one reads”](#what-one-reads) A proposal is a row in the same table as the roster suggestions, with its own kind: ```plaintext { id, kind, boardId, stageKey, rationale, target, estimatedEffect: { cards, agents, summary }, howToApply, registry: "read" | "unread", evidence, evidenceCount, firstSeenAt, lastSeenAt, resolvedAt, dismissedAt } ``` Roster suggestions and supply proposals are synced in one step, so neither resolves the other. List one kind with `GET /v1/suggestions?kind=change-model`. ## Why not file the change [Section titled “Why not file the change”](#why-not-file-the-change) A hand-off would need a new AgentPod verb first: a supply-request queue that a named service may file into under an allowlist, that a workspace admin approves or declines in the console, and whose approval runs the existing verb as that person, with an audit row. AgentPod has no such queue, so a proposal is something a person reads. # Operator API > Every route of SuperDispatch's HTTP API - boards, decisions, suggestions, the dataset and models - and how it is authenticated. SuperDispatch’s HTTP API is the operator’s. It listens on `PORT` (8796). In production it has no public address. ## Authentication [Section titled “Authentication”](#authentication) Every call but `/healthz` and the relay’s deliveries carries `Authorization: Bearer `. The token’s SHA-256 must equal `SUPERDISPATCH_OPERATOR_TOKEN_SHA256`; the service is never given the token itself. The check is constant-time and comes before anything is read, and a refusal is `401` and says nothing else. ```sh SUPERDISPATCH_OPERATOR_TOKEN_SHA256=$(printf '%s' "$OPERATOR_TOKEN" | shasum -a 256 | cut -d' ' -f1) ``` ## Routes [Section titled “Routes”](#routes) | Method | Path | What it does | | -------- | ------------------------------ | ---------------------------------------------------------------------------------- | | `GET` | `/healthz` | `{ ok: true, service: "superdispatch" }`; no token | | `POST` | `/v1/relay/events` | the event relay’s deliveries, signed with `TREATY_RELAY_DELIVERY_SECRET`; no token | | `GET` | `/v1/boards` | every board opted in, enabled or not | | `PUT` | `/v1/boards/:boardId` | opt a board in, or enable it again; no body | | `DELETE` | `/v1/boards/:boardId` | opt it out | | `GET` | `/v1/boards/:boardId` | one board | | `GET` | `/v1/decisions` | decisions, newest first: `board`, `card`, `before`, `limit` (1 to 200, 50) | | `GET` | `/v1/decisions/:id` | one decision (`pld_…`) | | `GET` | `/v1/suggestions` | suggestions and proposals: `board`, `kind`, `state` | | `GET` | `/v1/suggestions/:id` | one (`sug_…`) | | `POST` | `/v1/suggestions/:id/dismiss` | dismiss it | | `GET` | `/v1/dataset` | the labelled rows: `since`, `format` (`jsonl`, the default, or `json`) | | `POST` | `/v1/models/train` | train and store a model: body `{ since?, seed? }` | | `GET` | `/v1/models` | every stored model | | `GET` | `/v1/models/:id` | one model, with its metrics | | `GET` | `/v1/models/:id/shadow-report` | the model against the formula, in shadow | `kind` is any roster kind (`add-agent-to-board`, `lane-single-point`) or supply kind (`add-station`, `change-model`, `move-to-host`, `raise-capacity`). `state` is `open` (the default: not resolved, not dismissed), `dismissed`, `resolved` or `all`. ## What they answer [Section titled “What they answer”](#what-they-answer) * A board: `{ boardId, enabled, createdAt, updatedAt }`. See [Opting a board in](/run/opting-a-board-in/). * A decision: see [Decision records](/records/decisions/). * A suggestion: see [Roster suggestions](/records/roster-suggestions/) and [Supply proposals](/records/supply-proposals/). * The dataset and models: see [Learned routing](/records/learned-routing/). ## Examples [Section titled “Examples”](#examples) ```sh curl -X PUT -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards/brd_… # opt in curl -X DELETE -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards/brd_… # opt out curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/decisions?board=brd_…&card=card_…&limit=20" curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/decisions/pld_… curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/suggestions?board=brd_…&state=open" curl -X POST -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/suggestions/sug_…/dismiss curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/dataset?since=2026-11-01" > dataset.jsonl curl -X POST -H "authorization: Bearer $TOKEN" -d '{"seed":1}' http://127.0.0.1:8796/v1/models/train curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/models/mdl_…/shadow-report ``` # Opting a board in > How the operator chooses which boards SuperDispatch places cards on, what opting in changes, and how to opt out. SuperDispatch places cards only on the boards the operator opts in. There are none until one is, and a production deploy opts none in. ## Opt a board in [Section titled “Opt a board in”](#opt-a-board-in) ```sh curl -X PUT -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards/brd_… ``` No body. The answer is the board: `{ "board": { boardId, enabled, createdAt, updatedAt } }`. Opting in: * adds the board to every sweep, and looks at it at once without waiting for the next one; * sets the board’s **placement window** on SuperPipeline (`PUT /v1/boards/:id/scheduling`, with `SUPERDISPATCH_PLACEMENT_WINDOW_SECONDS`, 10 by default), so a card that just became ready is held back from pull for that long and placed on delivery of its `card.ready`. See [The placement window](/placement/overview/#the-placement-window). If SuperPipeline refuses the window, SuperDispatch tries again after five minutes, and the board is still swept meanwhile. ## Before you do [Section titled “Before you do”](#before-you-do) * The **roster** for the board in AgentPod is what is placed (with `SUPERDISPATCH_CANDIDATES=roster`, the default). A board with fewer than two rostered agents for a capability has no choice to make: its cards are recorded as `one-candidate` and left to pull, and the lane is flagged as a [roster suggestion](/records/roster-suggestions/). * The hub must list SuperDispatch in its `ROSTER_READERS`, and SuperDispatch’s credential must have `cards:read`, `dispatch:read` and `dispatch:reserve`. See [Running it](/run/running-it/#what-it-needs-from-the-others). * For the placement window to help, the **event relay** must deliver SuperPipeline’s events to SuperDispatch. Without it, the sweep alone places cards and most are lost to pull first. ## Watch it work [Section titled “Watch it work”](#watch-it-work) ```sh curl -H "authorization: Bearer $TOKEN" "http://127.0.0.1:8796/v1/decisions?board=brd_…&limit=20" ``` Each placed card has a [decision](/records/decisions/); each decision settles to an outcome on a later pass. The service log has a line per decision and per settlement: ```plaintext loop: brd_… card_… pld_… settled claimed by agt_… ``` ## Opt it out [Section titled “Opt it out”](#opt-it-out) ```sh curl -X DELETE -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards/brd_… ``` The board’s row stays, so it can be listed and enabled again with another `PUT`. Its decisions stay readable, holds already made lapse on their own, and its placement window goes back to none. ## List the boards [Section titled “List the boards”](#list-the-boards) ```sh curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards # every board, enabled or not curl -H "authorization: Bearer $TOKEN" http://127.0.0.1:8796/v1/boards/brd_… # one board ``` # Production > How a SuperDispatch release reaches production, what the release checks, how to roll back, and where its settings and store live. v0.0.1 runs in production, with no board opted in yet. ## How a release deploys [Section titled “How a release deploys”](#how-a-release-deploys) A `vX.Y.Z` tag on main deploys production through the suite’s shared release workflow (`.github/workflows/release.yml`, settings in `.github/release-config.json`, on the GitHub mirror only). Only the operator pushes tags. 1. It checks that forge CI passed for the tag’s commit, or runs `bun run check` at the tag when it cannot read that. 2. The production host runs that tag’s `deploy/production/release.sh` through its forced-command deploy key. 3. The release notes go up on GitHub with the tag’s section of `CHANGELOG.md`. A merge to main deploys staging. ## What the release checks [Section titled “What the release checks”](#what-the-release-checks) `release.sh` takes an online copy of the store, builds the tag’s image, recreates the service and checks, in order: 1. `/healthz`; 2. a token from Accounts with the service’s own credential, for SuperPipeline; 3. the hub, at `AGENTPOD_URL`; 4. the loop’s first sweep, in the log: `loop: first sweep, boards opted in, unread`. Those checks are the release’s smoke test: the service has no public address. If the release fails one, the release that ran before is put back, with the store’s copy when the new build had moved the schema, because a build refuses a database written by a newer one. ## On the host [Section titled “On the host”](#on-the-host) As root: ```sh bash /opt/superdispatch/deploy/production/smoke.sh v0.0.1 # the checks again, changing nothing bash /opt/superdispatch/deploy/production/rollback.sh v0.0.2 # put back the release before v0.0.2 cd /opt/superdispatch && SUPERDISPATCH_TAG=$(cat /etc/superdispatch/deployed) \ docker compose -f deploy/production/compose.yml logs --tail 20 dispatch ``` | What | Where | | -------------- | ------------------------------------------------------------------------------- | | Settings | `/etc/superdispatch/superdispatch.env`, written by the host’s provisioning | | Store | `/var/lib/superdispatch` on the host, mounted at `/data` | | Listens | `127.0.0.1:8798` on the host (8796 in the container): the operator API | | Relay delivery | on the event relay’s network: deploy the relay first | | The hub | through the host gateway, as the relay reaches it | | Deployed tag | `/etc/superdispatch/deployed`; the one before it, `/etc/superdispatch/previous` | ## What a deploy does not change [Section titled “What a deploy does not change”](#what-a-deploy-does-not-change) A deploy opts no board in. The settings that widen what SuperDispatch does are written once by the provisioning at their defaults, `SUPERDISPATCH_CANDIDATES=roster`, `SUPERDISPATCH_POLICY=off` and `SUPERDISPATCH_LEARNED=off`, and are the operator’s to change in `superdispatch.env`. A change takes at the next release, or at `docker compose ... up -d --force-recreate dispatch`. The start line names all three: ```plaintext superdispatch: listening on :8796, sweeping every 30s, candidates roster, policy off, learned routing off ``` # Running it > How to start SuperDispatch, and what it needs from SuperPipeline, AgentPod, Accounts, SuperWitness and the event relay. SuperDispatch is a Bun service with a SQLite store. It runs as a plain process beside the hub and reaches the other products through their public APIs. ## Start it [Section titled “Start it”](#start-it) ```sh bun install SUPERPIPELINE_URL=http://127.0.0.1:8787 \ ORG_PLANE_URL=http://127.0.0.1:8788 \ ORG_PLANE_SERVICE_CREDENTIAL=svc_…:… \ AGENTPOD_URL=http://127.0.0.1:3001 \ AGENTPOD_AUDIENCE=http://127.0.0.1:3001 \ TREATY_RELAY_DELIVERY_SECRET=… \ SUPERDISPATCH_OPERATOR_TOKEN_SHA256=$(printf '%s' "$OPERATOR_TOKEN" | shasum -a 256 | cut -d' ' -f1) \ SUPERDISPATCH_DATA_DIR=$PWD/.data \ bun apps/dispatch/src/main.ts curl http://127.0.0.1:8796/healthz ``` `/healthz` answers `{ "ok": true, "service": "superdispatch" }`. The start line names the settings that widen what it does: ```plaintext superdispatch: listening on :8796, sweeping every 30s, candidates roster, policy off, learned routing off ``` A missing or malformed setting stops it before it listens, naming the setting, never its value. Every setting is on [Settings](/run/settings/). It places nothing until a board is [opted in](/run/opting-a-board-in/). ## What it needs from the others [Section titled “What it needs from the others”](#what-it-needs-from-the-others) **Accounts.** One service credential for SuperDispatch, minted in the workspace (`ORG_PLANE_SERVICE_CREDENTIAL`), with the scopes: | Scope | For | | ------------------ | ----------------------------------------------------- | | `cards:read` | the board read | | `dispatch:read` | the eligibility read, and the agents’ load with `all` | | `dispatch:reserve` | the hold | | `policy:decide` | only with `SUPERDISPATCH_POLICY=on` | Its tokens are asked for SuperPipeline’s URL, the hub’s `ORG_PLANE_AUDIENCE` (`AGENTPOD_AUDIENCE`) and, when it is set, SuperWitness’s public URL. **SuperPipeline.** * `GET /v1/boards/:id`: the board and its cards; * `GET /v1/boards/:id/cards/:cardId/eligibility`: who could claim a card, and why not; * `PUT /v1/boards/:id/cards/:cardId/reservation`: the hold. A SuperPipeline without it refuses each hold, and the decision is recorded as `not-reserved`; * `PUT /v1/boards/:id/scheduling`: the board’s placement window, set on opt-in and opt-out; * with `SUPERDISPATCH_CANDIDATES=all`, `GET /v1/agents/load` (on `dispatch:read`). A SuperPipeline without it places the roster alone. **AgentPod.** * `GET /api/bridge/boards/:boardId/roster`, which the hub answers for a service listed in its `ROSTER_READERS`; * for stages that state a placement, `GET /api/catalog/stations`, for a service listed in its `CATALOG_READERS`. A hub without the catalog read leaves those placements unchecked. **SuperWitness** (optional). `GET /v1/runs`, which any token for its audience may read; no grant scope. Runs carry `stage_key` only from a SuperPipeline that reports it (`SUPERWITNESS_REPORT_STAGE`). Without it, scoring leaves the stage record out. **The event relay** (optional). A subscriber for `evt.*.superpipeline.>` delivering to `/v1/relay/events`, signed with `TREATY_RELAY_DELIVERY_SECRET`. A `card.ready` places its card at once, alone, with the board’s cached roster; any other event looks at its board two seconds later. Without the relay the sweep alone places cards, and most are lost to pull before it comes round. ## What it writes [Section titled “What it writes”](#what-it-writes) Its own store, under `SUPERDISPATCH_DATA_DIR`: the boards opted in, the decisions, and the suggestions. Outside it, two writes, both to SuperPipeline: the hold, and a board’s placement window. Nothing else, and nothing to AgentPod. ## Without it [Section titled “Without it”](#without-it) SuperDispatch never claims a card and never hands one to anybody. A hold it asks for lapses on its own within its tier’s window, and a card’s ladder ends with pull. If it stops, every hold it made lapses and every card is claimed by pull as before; SuperPipeline and AgentPod work the same with it or without it. # Settings > Every environment variable SuperDispatch reads, with its range and its default. SuperDispatch reads its settings from the environment once, at start. A missing or malformed setting stops it before it listens, naming the setting, never its value. A docs build checks this page against `apps/dispatch/src/config.ts`: every setting there is here, with its default. ## Required [Section titled “Required”](#required) | Setting | What it is | | ------------------------------------- | ---------------------------------------------------------------------------------------- | | `SUPERPIPELINE_URL` | Where SuperPipeline’s API answers: the ready cards, and the eligibility read for each | | `ORG_PLANE_URL` | Where the organization plane (Accounts) answers | | `ORG_PLANE_SERVICE_CREDENTIAL` | This service’s credential there, `svc_<20 hex>:` | | `AGENTPOD_URL` | Where AgentPod’s API answers: the roster, and each agent’s load and health | | `TREATY_RELAY_DELIVERY_SECRET` | The secret the event relay signs its deliveries to this service with | | `SUPERDISPATCH_OPERATOR_TOKEN_SHA256` | The SHA-256 (64 hex digits) of the operator’s bearer token; the token itself is not kept | ## Where it runs [Section titled “Where it runs”](#where-it-runs) | Setting | What it is | Default | | ------------------------ | ------------------------------------------------------------------- | -------------- | | `AGENTPOD_AUDIENCE` | The hub’s `ORG_PLANE_AUDIENCE`, when it differs from `AGENTPOD_URL` | `AGENTPOD_URL` | | `SUPERDISPATCH_DATA_DIR` | Where the store keeps its database; absolute | `/data` | | `PORT` | Where it listens | `8796` | ## Placement [Section titled “Placement”](#placement) | Setting | What it is | Range or values | Default | | ---------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------- | ------------- | | `SUPERDISPATCH_SWEEP_SECONDS` | Seconds between two sweeps of every opted-in board | 5 to 3600 | `30` | | `SUPERDISPATCH_RESERVATION_SECONDS` | Seconds the winner tier holds a card | 10 to 300 | `60` | | `SUPERDISPATCH_RUNNER_UP_SECONDS` | Seconds each runner-up hold lasts | 10 to 300 | `30` | | `SUPERDISPATCH_PLACEMENT_WINDOW_SECONDS` | The placement window set on each board opted in (0 sets none) | 0 to 30 | `10` | | `SUPERDISPATCH_TOP_K` | The most holds a card’s ladder makes before any match | 1 to 10 | `3` | | `SUPERDISPATCH_FAIRNESS` | How a sweep orders cards across boards | `round-robin`, `board-order` | `round-robin` | | `SUPERDISPATCH_CANDIDATES` | Who is considered: the roster alone, or also agents with their own token that report load | `roster`, `all` | `roster` | | `SUPERDISPATCH_STATION_FILTERS` | Whether a stage’s placement filters on AgentPod’s capability registry | `on`, `off` | `on` | | `SUPERDISPATCH_MIN_PROOF` | The weakest registry proof that satisfies a need | `probed`, `installed`, `declared` | `declared` | | `SUPERDISPATCH_POLICY` | Whether to ask Accounts’ dispatch policy (needs `policy:decide`) | `off`, `on` | `off` | ## History [Section titled “History”](#history) | Setting | What it is | Range | Default | | -------------------------------------- | ------------------------------------------------------------------ | -------- | ------------------ | | `SUPERWITNESS_URL` | Where SuperWitness’s API answers, for each stage’s history | a URL | unset: no history | | `SUPERWITNESS_AUDIENCE` | SuperWitness’s public URL, when it differs from `SUPERWITNESS_URL` | a URL | `SUPERWITNESS_URL` | | `SUPERDISPATCH_HISTORY_DAYS` | Days of a stage’s runs read | 1 to 365 | `30` | | `SUPERDISPATCH_HISTORY_MIN_SAMPLE` | The fewest runs (or verdicts) a stage rate is read from | 1 to 100 | `3` | | `SUPERDISPATCH_HISTORY_HALF_LIFE_DAYS` | Days in which a run’s weight halves | 1 to 365 | `7` | ## Suggestions [Section titled “Suggestions”](#suggestions) | Setting | What it is | Range | Default | | ------------------------------------- | --------------------------------------------------------------------------- | ---------- | ------- | | `SUPERDISPATCH_SUGGEST_EVERY_SECONDS` | The least time between two computations of the suggestions (0: every sweep) | 0 to 86400 | `0` | | `SUPERDISPATCH_SUGGEST_WINDOW_HOURS` | Hours of decisions the suggestions read | 1 to 720 | `24` | | `SUPERDISPATCH_STALL_MINUTES` | Minutes a card waits with nobody eligible before its lane is suggested for | 1 to 1440 | `30` | ## Learned routing [Section titled “Learned routing”](#learned-routing) | Setting | What it is | Range or values | Default | | ---------------------------------- | ---------------------------------------------------------------------------------- | --------------------- | ------- | | `SUPERDISPATCH_LEARNED` | Off, shadow (record the model’s choice, hold the formula’s), or on behind the gate | `off`, `shadow`, `on` | `off` | | `SUPERDISPATCH_LEARNED_MIN_ROWS` | The fewest training rows a model needs to place | 1 to 10000000 | `500` | | `SUPERDISPATCH_LEARNED_MIN_SHADOW` | The fewest settled shadow decisions a model needs to place | 1 to 1000000 | `100` | | `SUPERDISPATCH_LEARNED_MARGIN_PCT` | Points a model must beat the formula by in shadow to place | 0 to 100 | `5` | ## In production [Section titled “In production”](#in-production) The provisioning writes the three settings that widen what SuperDispatch does once, at their defaults: `SUPERDISPATCH_CANDIDATES=roster`, `SUPERDISPATCH_POLICY=off` and `SUPERDISPATCH_LEARNED=off`. They are the operator’s to change. See [Production](/run/production/). # Status and limits > Where SuperDispatch stands. v0.0.1 runs in production with no board opted in yet, and what it does not do yet. SuperDispatch is early. These docs describe what the current source does, and this page says what it does not. ## Where it stands [Section titled “Where it stands”](#where-it-stands) * **Placement is proven end to end on staging**: filtering, scoring, holds in tiers, fairness across boards and the placement window, with decisions settled against what became of each card. * **v0.0.1 runs in production, with no board opted in yet.** A deploy opts no board in, so in production it sweeps nothing until the operator [opts a board in](/run/opting-a-board-in/). * **The settings that widen what it does are off**: the candidate set is the roster (`SUPERDISPATCH_CANDIDATES=roster`), the dispatch policy is not asked (`SUPERDISPATCH_POLICY=off`) and learned routing is off (`SUPERDISPATCH_LEARNED=off`). ## What it does not do yet [Section titled “What it does not do yet”](#what-it-does-not-do-yet) **The dispatch policy is not enforced on pull.** SuperPipeline does not check Accounts’ dispatch policy when an agent claims. With `SUPERDISPATCH_POLICY=on`, SuperDispatch will not hold a card for an agent the policy refuses, but that agent can still claim the card by pull. Until SuperPipeline enforces it, policy here is advice like the rest of SuperDispatch. See [The dispatch policy](/placement/policy/). **Decisions are not published as events.** The charter decision says every decision is published through treaty’s relay, so SuperWitness can audit it and chat can show it. That needs a decision event in treaty’s catalogue first. Until then, read decisions on the [operator API](/run/operator-api/). **Suggestions do not reach chat.** Roster suggestions and supply proposals are kept in SuperDispatch’s own store and read on the operator API (`GET /v1/suggestions`). Nothing posts them to a board’s room yet. **Station filters need AgentPod’s capability catalog.** A stage’s `placement` (isolation, models, tools, credentials) filters candidates only when AgentPod answers its capability registry (`GET /api/catalog/stations`) for this service. A hub without the catalog read, or one that refuses it, leaves those needs unchecked: the pass logs that and places as if none were stated. See [Station filters](/placement/station-filters/). **Learned routing needs data first.** The dataset, training and shadow mode exist, but a model fitted on a few dozen decisions is noise. It is off by default and should run in shadow long before it places anything. See [Learned routing](/records/learned-routing/). **Cards carry no labels yet.** The dispatch policy is asked about an agent, a board stage, labels and capabilities, but SuperPipeline cards have no labels, so none are sent. **The stage record joins claims, not results.** A learned model’s target is whether the agent claimed the card. Whether the claimed run then succeeded is SuperWitness’s to say, and is not joined into the dataset yet. The [scoring formula](/placement/scoring/) does read finished runs from SuperWitness, when it is set. **The reservation window is not settled.** The charter left its length to be measured against real claim latency. The defaults (60 seconds for the winner, 30 for each runner-up, a 10-second placement window) are where it starts. # What SuperDispatch is > The suite's scheduler. It reads cards from SuperPipeline and agents from AgentPod, decides which agent should take each ready card, and records why. SuperDispatch is the suite’s scheduler. When a SuperPipeline card is ready, it decides which agent should take it, and records why. Before it, placement happened anyway, scattered and mostly by hand: a person added an agent to a board’s roster in AgentPod, and the agent pulled any card whose capability matched. Whoever asked first got the card. Nothing compared candidates, nothing knew that one agent was busy and another idle, and nothing recorded why a card went where it went. On a board with one agent per capability, one stuck agent stalled the lane. ## What it does [Section titled “What it does”](#what-it-does) On each board the operator [opts in](/run/opting-a-board-in/), every 30 seconds and as soon as the event relay delivers one of the board’s events, SuperDispatch: 1. **reads the board** from SuperPipeline and settles what became of each card it held; 2. **queues the ready cards** and takes them fairly across boards, longest-waiting first; 3. **places each card**: it [filters](/placement/filters/) every agent of the workspace, each rejection carrying a reason code, [scores](/placement/scoring/) the ones left, and asks SuperPipeline to [hold the card](/placement/holds-and-tiers/) for the best for a short window; 4. **records the decision**, with every candidate and the reason for each ([Decision records](/records/decisions/)). After each sweep it reads its own decisions back and flags lanes that rest on one agent or go unplaced for want of supply: [roster suggestions](/records/roster-suggestions/) and [supply proposals](/records/supply-proposals/), advice for a person. ## Its rules [Section titled “Its rules”](#its-rules) These come from the charter decision [SuperDispatch owns the Scheduler plane](https://github.com/SuperJackfruitLabs/charter/blob/main/decisions/2026-11-12-superdispatch-owns-the-scheduler.md) (2026-11-12), and the code holds each one. * **It advises; SuperPipeline holds.** For each ready card SuperDispatch names a preferred agent. SuperPipeline holds the card for that agent for a short reservation that expires on its own, checked when a claim is made. When the reservation lapses, or SuperDispatch is absent, any matching agent may claim the card as before. * **A reservation outranks priority.** The reserved agent gets its reserved card even when a higher-priority card with no reservation is waiting. SuperDispatch already weighed priority when it chose. * **AgentPod-rostered agents only, at first.** Agents that claim with a token of their own are recorded as outside the candidate set (`not-on-roster`) and keep working by pull. Widening the set is a setting, `SUPERDISPATCH_CANDIDATES=all`, and `roster` stays the default. * **The idle filter is the workspace-wide cap.** It holds a card only for an agent with no work in hand anywhere, and at most one card for an agent at a time. SuperPipeline’s per-board concurrency stays as it is. * **A hold relaxes in tiers**: the winner, then the runner-ups one at a time, then any match. Each step is its own decision, labelled with its tier. * **A busy board does not starve a quiet one.** One sweep places a card from each board in turn before any board takes a second free agent. * **Every decision is a record**: the card, the chosen agent, every rejected candidate with its reason, and the score that ranked the rest. * **It reads, and never edits a roster or a station.** It may propose a roster change or a supply move; a person makes it. No client it holds of another product has a roster write, and a test calls every method of every client and fails on any write but the reservation. * **Nothing requires it.** SuperPipeline without SuperDispatch is the same board it is now, and AgentPod’s roster works without it. ## What it is not [Section titled “What it is not”](#what-it-is-not) * **Not a dispatcher.** It never claims a card and never hands one to an agent. Direct dispatch was ruled out: it would replace the pull that works today and make the scheduler a dependency. * **Not inside SuperPipeline or AgentPod.** A board would otherwise need to know about stations, load and health, and the fleet console would decide work it does not own. It runs as its own service and reads both through their public APIs. * **Not an owner of anyone’s data.** It keeps its own store: the boards opted in, its decisions, and its suggestions. Everything else it reads. ## What it is made of [Section titled “What it is made of”](#what-it-is-made-of) A Bun service in TypeScript, Apache-2.0 licensed, with a SQLite store. Placement is pure functions over plain inputs and does no I/O; the loop reads the other products and hands placement the facts. The source is at [github.com/SuperJackfruitLabs/superdispatch](https://github.com/SuperJackfruitLabs/superdispatch).