Which model a box thinks with

A box has one model policy and any number of chats that may disagree with it. This describes where each lives, who reads it, and when a change takes effect.

Three settings, one file

_config/box.json holds all of it, beside agentEngine:

FieldWhat it decides
agentModelThe box default model — what chat and the reactor use when nothing more specific applies.
smallModelThe model for the cheap structured passes (chat review, retro observation, triage). Missing means the efficient tier for whichever engine runs the pass, or the box default when that is an OpenRouter model. There is no admin control; set it in the file or ask the box agent.
enginesWhich harnesses a new chat may be started on. Missing means only agentEngine; the default engine can never be disabled.

A pi box has no tier table. It runs only added OpenRouter models and must pin one as its default; see the pi engine.

Two levels

The box default is agentModel in _config/box.json, beside agentEngine. It holds a concrete model id (claude-sonnet-5-5, gpt-5.6-terra, …). Missing means the strong tier for the box's engine — Opus on Claude, Sol on Codex — not "whatever the harness picks". A box always has a default it can name, so the chat UI can say what a follower will run and the model dial has something to compare against; deferring to the harness left both blank on every unpinned box (boxholder, 2026-09-14). It is owner-only to change, and the change is committed to the box's git history like any other config edit.

A chat's own model is .beebox/chat-models/<sessionId>.json, written only when that chat picks one. Absence is a state, not a gap: a chat with no file follows the box default, and keeps following it — so pinning a new default changes what every unopinionated chat starts on.

What reads the policy

ReaderWhich settingWhen it resolves
A chatagentModelWhen its subprocess starts (cold start or restart).
The reactoragentModelOnce per run, before its agents are created.
A procedure step with no model:, triage research, migration repairagentModelPer agent run. An unpinned box leaves these to the harness default.
A procedure step with a tier, the procedure review judgethe tier table, or both slots on an OpenRouter box (below)Per agent run.
Chat review, retro observation, triagesmallModelPer run.

A caller that names a concrete model still gets that model. The policy is read at the call sites above, not inside createAgent. (Why: docs/implemented-plans/model-engine-policy.md.)

The small passes used to name "haiku", a Claude nickname the Codex harness forwarded to the Codex SDK verbatim. Nothing names a model as a bare string any more: every value passes through the resolver, which cannot produce a name the running engine does not know.

Resolution

src/core/model-policy.ts is the only ladder. In order:

  1. The chat's own model, if it has one this engine can run.
  2. The box default — exactly, if this engine offers it; otherwise the model at the same tier (efficient/balanced/strong/strongest, from src/shared/agent-models.ts), so a box that switches harness keeps a policy rather than silently losing one. A box that pinned nothing takes the strong tier here (boxDefaultModel).
  3. Nothing — reached only when a pin exists but names no model any engine offers. A box saying something unreadable is not a box saying nothing, so that case is not quietly given the default.

Retired model ids are carried forward (normalizeModelId) before any of this, and a value no engine offers is rejected at loadBoxModel with one warning per config load.

Changing it does not interrupt anything

Pinning a default restarts no session. A chat that is already running keeps the model its subprocess started with and picks the new default up the next time it starts cold; chat.status reports the model actually in force plus a pendingModel naming what a restart would change it to.

Choosing a model for one chat is the other case and does restart it — an explicit choice is an instruction to that conversation, deferred past a turn in flight so no response is lost.

A chat's engine is fixed at birth

A chat's model can change whenever. Its engine cannot: transcripts live in different stores per engine (a Codex chat's history is Codex's own thread; a Claude chat's is husk JSONL on disk) and models are engine-scoped, so switching mid-chat would silently change two things at once and strand the transcript.

So the engine is chosen before the first message, in the same picker as the model: each enabled engine gets a section, and picking a model under one is how you choose it. resolveStartEngine enforces the rule where it is read — a recorded engine always wins over a requested one.

Two carriers, because only Claude accepts an id the browser chose:

Switching engines therefore restarts a chat that has not spoken yet: the coined id is abandoned (its reservation expires on its own) and the chat comes back through ?session=new carrying the choice. Nothing is lost, because this is only reachable before the first message.

How much context a run gets

The same question — what does this invocation get — covers context, and it has one lever: loadBoxContext. The SDK loads the box's AGENTS.md, generated agent guide and .claude/rules/ by default, which is right for the reactor, procedure runs and chat. The four small structured passes set it false: measured on the test box that context is ~9,700 words on every invocation, and a pass emitting a title or a verdict cannot use it. It is an opt-out, never a default, and it is Claude-only — the Codex harness has no equivalent.

Where the controls are

History

.beebox/chat-model.json was the box-wide pointer a chat inherited when it had no id yet — which every chat on a Codex box did, since coined ids are Claude-only. The chat-model-to-box-config migration folds it into agentModel and removes it.

GLM (Z.ai) models

The claude engine can also run GLM models — glm-5.3 and glm-5.3-flash — through Z.ai's Anthropic-compatible endpoint. A box opts in by pinning one (agentModel, smallModel, or a chat's model dial). Every run on a GLM model resolves the machine secret store's glm key and injects it into the subprocess environment; a missing or ungranted key fails the run with the setup commands (bbx secrets set glm, bbx secrets grant <box> glm) and never falls back to first-party.

Z.ai's coding-plan policy inspects request content and flags assistant-style use — choosing GLM for a box accepts that posture for its content. Sessions may move between providers freely: transcripts are local, so a conversation that starts on GLM can continue on Claude and the reverse. The SDK prices GLM runs with first-party tables, so their chat results carry no total_cost_usd; read spend from Z.ai's own usage dashboard.

OpenRouter models

The claude engine can also run models OpenRouter carries (DeepSeek, Kimi, Qwen, and others) through OpenRouter's Anthropic-compatible endpoint. They are billed per use, so a box never runs one because a key exists. The owner adds each model in Admin → OpenRouter models, which writes openrouterModels: [{ id, label }] to _config/box.json. The add is checked against OpenRouter's public catalog: the id must exist and support tool calling.

Tested through a full agent turn (tools, structured output, resume), 2026-09-19: moonshotai/kimi-k2-0905:exacto, deepseek/deepseek-v3.2, qwen/qwen3-coder. Other models may fail turns that need tools.