> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime Controls

> Session governance declared on the agent: concurrency slots and automatic slot replacement, spend caps and their enforcement, and model selection with fallback chains.

Three groups of agent-file fields govern how an agent's sessions run: `concurrency` (with `replace`, `onReplace`, and `manages`) caps and maintains a single live session; `spendCaps` bounds what sessions may spend; and `model` (with `reasoningEffort`) selects the model and its failover behavior. This page covers all three as implemented. For every other agent field, see [Agent file](/reference/agent-file).

## Concurrency

```yaml theme={null}
concurrency: 1        # live-session cap; only 1 is supported
replace: auto         # platform may drain + replace stale/failed members
onReplace: |          # rebuild-on-wake prompt for replacements
  Re-derive state from your durable sources before acting.
manages:              # session-control authority, by agent type
  - staff-engineer
```

<ParamField path="concurrency" type="integer">
  Cap on live sessions for this agent. **Only `1` is accepted** — the schema rejects anything else with "Only `concurrency: 1` is supported; larger pools are a deliberate non-goal for now". A concurrency-1 agent keeps at most one live session, called the *slot member*.
</ParamField>

<ParamField path="replace" type="&#x22;auto&#x22;">
  Opts the agent into platform-managed slot replacement. `auto` is the only value, and it is an assertion by you: the agent's state is fully reconstructable from external sources (chat threads, session lists, bindings), so the platform may stop a stale or failed member and spawn a replacement on the latest spec. Requires `concurrency` to be set.
</ParamField>

<ParamField path="onReplace" type="string">
  The rebuild prompt delivered as a replacement session's start message, trimmed, 1–20,000 characters. Supports `file:` references and the `append:` directive like other prompt fields. Requires `replace: auto` (nothing else consumes it); a generic fallback prompt is used when undeclared.
</ParamField>

<ParamField path="manages" type="string[]">
  Agent-type names (max 64) whose sessions this agent may stop within its project. Authority belongs to the agent *type*, not to spawn provenance — a replacement session controls sessions its predecessor spawned. Self-control is always implicit, and unknown names are inert rather than invalid, so the managed agent can be applied or removed independently.
</ParamField>

### The slot at runtime

The slot is an `agent.singleton` session binding claimed inside every session-creation transaction. While a live member holds it:

* A trigger routed `kind: spawn` **converts to a delivery** to the live member instead of creating a second session.
* A manual start (UI or `auto.sessions.spawn`) is **refused with a 409** naming the live session id.
* Agent-addressed messages (`auto.sessions.message` with an agent name) resolve to the live bound member, so peers keep reaching the agent across replacements without tracking session ids.
* A `deliver` trigger with no `routeBy` resolves the slot member; this shape is only valid on agents with `concurrency: 1`, or when a deliver-without-`routeBy` trigger on the agent declares `onUnmatched: spawn` — that spawn claims the slot itself.

### Slot replacement

A platform reconciler (5-second cadence) keeps `replace: auto` agents healthy in three phases:

1. **Release.** A member that archives itself leaves the pool at the end of its current turn; the slot frees within one sweep.
2. **Drain.** When the applied agent spec drifts from the spec a live member was spawned with, the member receives one durable warning message: the definition changed, automatic replacement happens in **five minutes**, and it should checkpoint work and archive cleanly with a handoff — or stop itself with respawn intent to reload immediately. A member still live at the deadline is stopped with respawn intent. A spec that changes again mid-grace restarts the window against the new target; a spec that reverts cancels it.
3. **Replace.** A member that failed (within a crash-loop budget of 3 failures per 30-minute sliding window) or was stopped with respawn intent gets a direct replacement: a slot-claiming spawn on the latest spec whose start message carries `onReplace`. A plain manual stop without respawn intent is the operator opt-out — nothing respawns.

Replacement is deliberately not replay: the successor shares no transcript, memory, or sandbox with its predecessor. Losslessness comes from the `onReplace` rebuild plus `onUnmatched: spawn` routing — during the brief window when no member is live, triggers routed `onUnmatched: spawn` (mentions, subscribed replies) spawn the successor carrying the event, while `onUnmatched: drop` deliveries (reactions, heartbeats) are dropped by design.

A production example, condensed from this repo's chief-of-staff agent:

```yaml .auto/agents/chief-of-staff.yaml theme={null}
name: chief-of-staff
concurrency: 1
replace: auto
manages:
  - staff-engineer
  - chief-of-staff
onReplace: |
  You are a fresh chief-of-staff session, spawned to replace a predecessor.
  The swap left a window where no chief was live, so rebuild state before
  doing anything else: consume the predecessor's handoff, list its sessions
  with mcp__auto__auto_sessions_list, and verify load-bearing claims against
  live sources before acting.
```

## Spend caps

```yaml theme={null}
spendCaps:
  dailyUsd: "25"
  monthlyUsd: "250"
  maxPerSessionUsd: "10"
```

All three fields are optional; omitted values mean no cap. Values are **exact nonnegative decimal strings** with at most 10 integer and 10 fractional digits, matching the usage ledger's `numeric(20,10)`. YAML numbers, negatives, and exponent notation are rejected at authoring time — quote the value. Zero is valid and means a configured zero-dollar cap.

<ParamField path="spendCaps.dailyUsd" type="string">
  Agent-type cap on spend per UTC calendar day, summed across all of the agent's sessions. Resolved from the *active* agent resource at enforcement time, so applying a spec change affects live sessions immediately.
</ParamField>

<ParamField path="spendCaps.monthlyUsd" type="string">
  Agent-type cap per UTC calendar month. Same resolution semantics as `dailyUsd`.
</ParamField>

<ParamField path="spendCaps.maxPerSessionUsd" type="string">
  Lifetime cap for a single session. Copied into the session record at creation and **immutable for that session** — later spec changes apply only to new sessions.
</ParamField>

Daily and monthly windows reset at UTC calendar boundaries (midnight UTC; the first of the month). There is no organization-timezone option.

### Enforcement

When any cap is at or over its limit (an *effective hit*), the platform stops new spend without killing anything:

* **Session creation** is refused while an agent daily/monthly cap (or a requester cap) is hit, with a message pointing at the admin override path.
* **Turn-starting commands** — `start`, `startWithMessage`, `message`, `answer` — on existing sessions are deferred rather than dropped: the command stays pending and is retried on a one-minute backoff until the cap clears. `stop` and `interrupt` are never deferred, so a capped session can always be terminated.
* **Idle capped sessions are parked** with reason `billing_spend_cap_exceeded` and a resume condition tied to the cap: they wake automatically when the window resets or an override lands. A session mid-turn finishes its turn first — enforcement decisions happen between turns.

Organization admins can lift a cap without editing YAML: reversible **overrides** exist at the agent and session level (with a reason and optional expiry), granted from the web app or API. Requester-level spend caps — configured per human requester in organization settings, not in the agent file — are evaluated alongside agent and session caps in the same enforcement pass.

<Note>
  Spend caps bound model usage recorded to the session. They are a budget-control mechanism, not a security boundary: a capped agent's session keeps its sandbox and pending state, and resumes exactly where it deferred once the window resets.
</Note>

## Model selection

```yaml theme={null}
harness: claude-code
model:
  provider: anthropic
  id: claude-sonnet-5
reasoningEffort: high
```

<ParamField path="model" type="object">
  Optional. When omitted, the harness default applies: `claude-code` runs `anthropic`/`claude-opus-4-8`; `codex` runs `openai`/`gpt-5.6-sol`. When present, `id` is required.
</ParamField>

<ParamField path="model.provider" type="&#x22;anthropic&#x22; | &#x22;openai&#x22; | &#x22;openrouter&#x22;">
  Defaults to the harness's default provider. Each harness accepts a fixed set: `claude-code` → `anthropic` only; `codex` → `openai` or `openrouter`.
</ParamField>

<ParamField path="model.id" type="string" required>
  The model to run. Curated per provider:

  | Harness / provider          | Models                                                                                                                                                                                |
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `claude-code` / `anthropic` | `claude-opus-4-8` (default), `fable`, `claude-fable-5`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001` |
  | `codex` / `openai`          | `gpt-5.6-sol` (default), `gpt-5.5`, `gpt-5.3-codex`                                                                                                                                   |
  | `codex` / `openrouter`      | Open: any `org/model` OpenRouter slug (e.g. `z-ai/glm-5.2`, `moonshotai/kimi-k2.7-code`)                                                                                              |

  OpenRouter ids are deliberately uncurated — the catalog is long-tail and changes independently of auto releases; the live service verifies existence against OpenRouter's `/models`.
</ParamField>

<ParamField path="reasoningEffort" type="string">
  A top-level agent field (sibling of `model`, not nested in it). Allowed values and defaults are harness-scoped: `claude-code` accepts `low | medium | high | xhigh | max` (default `high`); `codex` accepts `minimal | low | medium | high | xhigh` (default `medium`). Some OpenRouter models narrow the set further (for example `z-ai/glm-5.2` accepts only `high | xhigh`); an effort the resolved model does not support fails validation at apply time.
</ParamField>

Model routing is snapshotted when a session launches: applying a `model` change affects **new sessions only**, and a session whose model was explicitly overridden at start has no fallback chain.

### Fallbacks

Two mutually exclusive, opt-in fallback surfaces live on `model`. Both exist so a transient provider overload does not fail a turn; both are inert when absent.

<ParamField path="model.fallbacks" type="array">
  Same-provider fallback, for any harness. An ordered chain of 1–2 entries, each `{ id }` or `{ provider, id }`:

  ```yaml theme={null}
  model:
    provider: anthropic
    id: fable
    fallbacks:
      - id: claude-opus-4-8   # provider defaults to the primary's
  ```

  Validation enforces: every entry resolves under the same harness rules as the primary; resolves to the **same provider** as the primary; ids are distinct from the primary and each other; and the agent's `reasoningEffort` must be valid for every fallback (the gateway rewrites only the model, never the effort).

  At runtime, when a request naming the primary exhausts the gateway's transient-retry budget (HTTP 429/529, roughly 20 seconds of cumulative backoff per model), the gateway rewrites the request to the next fallback and re-runs the loop. The first activation opens a **5-minute sticky window** during which primary-named requests go straight to the fallback; after the window, the next request probes the primary again — fail-back is automatic, with no operator action. Requests for other models inside the session (subagents, title generation) pass through untouched.
</ParamField>

<ParamField path="model.openrouter" type="object">
  OpenRouter-native fallback, valid only when the resolved provider is `openrouter` (codex agents today). Mutually exclusive with `model.fallbacks`.

  ```yaml theme={null}
  model:
    provider: openrouter
    id: z-ai/glm-5.2
    openrouter:
      models:              # 1–3 fallback slugs; the primary is prepended
        - moonshotai/kimi-k2.7-code
      provider:
        sort: throughput   # price | throughput | latency
        allowFallbacks: true
  ```

  * `models` (required): 1–3 OpenRouter slugs, distinct, excluding the primary id (it is prepended automatically). OpenRouter fails over across the chain natively.
  * `provider` (optional) routing preferences: `allowFallbacks` (boolean), `order` (preferred upstream providers, in order), `sort` (`price` | `throughput` | `latency`), `only` / `ignore` (upstream provider allow/deny lists).
</ParamField>

### Observing a fallback

When a fallback serves a response, the platform is explicit about it: the response carries an `x-auto-model-fallback: <id>` header, the session's conversation gets one status entry per activation ("Model fallback activated: running on fallback model … (primary … overloaded)."), and the `auto.sessions.summary` MCP tool reports a `modelFallback` object with the latest activation time and the fallback model. Usage is attributed to the model that actually served each request.

## Related pages

* [Agent file](/reference/agent-file) — the full agent field reference, including session policy (`session.archiveAfterInactive`)
* [Triggers](/reference/triggers) — `routing`, `routeBy`, and `onUnmatched`, which concurrency semantics build on
* [Sessions](/concepts/sessions) — the session lifecycle these controls govern
