Skip to content
View as .md

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).
See threads, dispatch.

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.”