# Configs

Per-scope behaviour (trigger modes, model defaults, prompt overrides, feature flags) as one merged record — no new table per knob. A config is a deep-partial `ConfigPatch` stored at house or thread scope. There's no app-scope row: `DEFAULT_CONFIG` lives in source and is edited via PR.

```
configs { id, scope_kind: 'house'|'thread', scope_id, patch: ConfigPatch }
UNIQUE(scope_kind, scope_id)
```

Resolution walks `DEFAULT_CONFIG ← house ← thread`: collect ancestor scope ids → one SELECT → deep-merge patches in chain order → strip remaining `null` leaves → overlay onto `DEFAULT_CONFIG`. It returns `{ config: ResolvedConfig, chain }`, where `chain[0]` is always `{ scope: 'app', patch: DEFAULT_CONFIG }`, so "why is this value X?" is answerable from one call. The resolver is sync; command-level wrappers fetch then delegate.

Merge semantics: plain objects deep-merge; arrays replace (configs are settings, not collections). To override, write a non-null leaf; to reset to the code default, write `null` (beats any ancestor); to inherit, omit the key. `undefined` is stripped on write. Schema is growth-friendly — every field `.optional()`, sections `.passthrough()` so old patches survive renames. No `version` field; renames are one-shot migrations. Sections in v1: `dispatch`, `llm`, `flags`.

Per-agent narrowing lives inside the patch (`dispatch.perAgent[agentId]`), not as a fifth scope — agents are cross-cutting (one agent, many houses) and don't fit a parent_id tree. The dispatch path resolves the chain, then applies `perAgent[currentAgentId]` last. Promoting agent to a real overlay later is additive: one resolver param, one merge step, no schema break.

```
arbe config get <kind> [<ref>] [--raw]              # kind: house | thread | app
arbe config set <kind> <ref> --patch '<json>'
arbe config delete <kind> <ref>

GET    /api/{houses,threads}/:id/config              # resolved (chain merged); ?raw=1 = patch only
PATCH  /api/{houses,threads}/:id/config              # deep-merge body into stored patch
DELETE /api/{houses,threads}/:id/config              # clear this-scope patch (chain still flows)
```

`get app` reads `DEFAULT_CONFIG` from code; `set app` / `delete app` don't exist. CLI and HTTP both call `setConfig` server-side, so there's exactly one merge site (read-then-write, not transactional — concurrent PATCHes on the same scope can race; revisit if it matters). Never 404 just because no row exists at this scope — the resolved chain still has defaults and ancestors. Permissions: read-on-parent implies read+write on its config.

Code: `@arbe/core/schemas/config.ts` (`ConfigSchema`, `DEFAULT_CONFIG`), `@arbe/core/configs.ts` (resolve + `setConfig`).<br>See [threads](./threads.md), [dispatch](./dispatch.md).

If your feature needs per-scope behaviour, the answer is almost always "add a section to `Config`," not "add a column" or "add a `scope_X` table."
