runtime.pool())
with a different unit of address: a team, per tenant. Legacy orchestration is frozen,
not removed — every existing route, CLI command and config keeps answering.
The address is a pair
A team is declared once and instantiated per tenant, so every runtime operation carries both:runtime.durable() and runtime.hermes() are a closed two-member union;
runtime.hermes() is the legacy runtime, declarable and deprecated. runtime.pool()
is untouched.
Which runtime is this tenant on?
Every answer below depends on it. The decision is per tenant, a single column —company_containers.sidecar_url — with no third state and no global switch.
Exactly one production path points that column at the durable runtime: naive up
placing a newly declared team (registerVettaRuntime(), called only from
services/placement.ts when a teams: block declares runtime.durable(...) and the
operator supplies NAIVE_DURABLE_CREDENTIAL_<TEAM> out of band). Every other tenant
reads provider: "hermes". Placement permits only two states — no container row (place
destroys nothing) or already durable (converge, never re-register) — and refuses a
claim in flight or a live legacy container, because registering overwrites the tenant’s
legacy coordinates. Moving an existing tenant is a runbook, not a request:
POST …/migrate refuses on both runtimes.
Every response on this surface carries provider ("durable" or "hermes"), and
every refusal carries error.details.runtime — read every claim on this page against
one tenant.
Is the durable runtime on this deployment?
What reads real rows today
Of the 33 operations on this surface, a durable tenant is served 25 and a hermes tenant is served 14. Eight are refused on both. The 14 a hermes tenant is served answer from real tables (tasks_mirror, task_runs_mirror, task_events_mirror, agents,
tenant_spend_events) with keyset cursors, and that count is pinned by
ci/runtime-surface-counts.test.ts.
The hermes 14 is a frozen number and is meant to stay one.
The durable runtime gaining eleven operations cost the legacy surface nothing: no route
it answered was moved, narrowed or re-pointed, and the gate above fails if that stops
being true.
It has moved exactly once, from 13 to 14, and the operation that moved it is
GET /v1/teams, which refused until the JSONB key naming a declared team was renamed
(packages/db/migrations/065_agents_config_team_key.sql). It reads agents and reaches
neither runtime, so both lanes gained it at once — and it cost the frozen runtime nothing
either, because it dispatches to no runtime at all.
POST …/approvals/{id}/decide is a second address for one write path: it calls the
same executeApproval / denyApproval the existing approvals routes call.
GET …/board?status= filters in memory on both runtimes (on hermes the wire says
filtered_in_memory: true). The runtime’s six states and this surface’s six are
different partitions, so ?status=unverified on a durable tenant is a valid query that
can only ever match nothing — see Board & cards. A
durable card carries runtime_status (todo ready doing review done blocked) beside
the mapped status (open claimed awaiting_check done unverified blocked) rather than
silently substituting one for the other.
The eleven a durable tenant gets and a hermes tenant does not
These reach the durable runtime through the control-plane seam (${sidecar_url}/control/{company}/{tenant}{path}). For a hermes tenant each answers
501 not_configured with error.details.runtime: "hermes" and the legacy route that
does the equivalent job.
Three of the shared reads —
GET …/board, GET …/board/{card}, GET …/runs/{id} —
serve both runtimes but from different stores: the mirror for a hermes tenant, the
runtime’s own board for a durable one.
POST …/stop
POST …/stop stops the team’s dispatch loop and returns cancelled (scheduled ticks
removed), in_flight (attempts already running) and is_pause_not_decommission: true.
It marks the team stopped, cancels the recurring tick, and fences the tick so a board
transition arriving afterwards cannot re-arm it (the stop is recorded before the
cancel). It does not recall an attempt already handed to a member — that attempt
runs to its end and spends what it spends, which is why the answer carries in_flight.
start-loop is the way back. There is deliberately no per-run kill
(POST …/runs/{id}/stop refuses on the durable runtime).
GET …/runs/{id}/stream has no trace_id yet (X-Naive-Trace-Unavailable) and is not
metered on open; the runtime’s own ceilings bound it (300s stream, 250ms poll, 120
reads/min at the front door).
What is declared and refuses on both runtimes
The rest of the surface answers501 not_configured with error.details.missing
naming each absent dependency and error.details.runtime naming which runtime the
refusal is about. It never returns { items: [] } for a refusal.
Governance is the same governance
Every operation sits behind the same session-or-key authentication and the same approvals machinery as the rest of the API — no second policy engine and no runtime-specific bypass. See the governance gateway and Approvals.Related
- The brain — what a team’s
brain.partition()binds - Approvals — the queue
…/approvalsreads - The decision ledger — why
decision_idis null here - Event & trigger router — inbound events, still on the legacy lane