Skip to content

Configuration Reference

Every configuration key the Engine reads, where it lives, and whether it is live or inert. This enumerates what §09 — Configuration curates — that section explains why a setting exists and how to reason about the model chain; this page is the exhaustive list. Where they disagree, the code wins and the delta is flagged.

Keys resolve from two worlds: file-based (appsettings*.json + environment variables, bound at startup) and the SystemSettings table (runtime-mutable, no restart). A handful of values are neither — they are hardcoded literals, marked as such below.

Live vs. inert

Live = read by application code at runtime. Inert = defined, bound, or seeded but never read (or shadowed by a higher layer). Inert keys are documented because setting one and expecting an effect is a common dead end. Don't tune a knob this page marks inert.

Resolution order

File configuration is layered lowest-to-highest; a key set higher wins. Built in Program.cs:

# Layer Restart to change?
1 appsettings.json (committed defaults) Yes
2 appsettings.{Environment}.json (per-ASPNETCORE_ENVIRONMENT overlay) Yes
3 User Secrets (Development only) Yes
4 Environment variables Yes
5 Command-line args Yes
6 appsettings.Local.json (gitignored; appended last) Yes
7 SystemSettings table (consulted at runtime by specific services) No

Two consequences worth internalizing: appsettings.Local.json is added after the default chain, so it outranks environment variables and command-line args. And SystemSettings is not part of the file pipeline — each service decides whether to consult it, using an emptiness gate (a meaningful row wins; an empty row falls through to the file value).

appsettings.Linux.json is not loaded because the host OS is Linux. It loads only when ASPNETCORE_ENVIRONMENT=Linux. Storage paths additionally pass through a post-configure step that expands ${VAR} / %VAR% tokens and falls back to platform defaults (C:\aetherium\… on Windows, /var/aetherium/… on Linux).

Files present

File Loaded when
appsettings.json Always (base)
appsettings.Development.json ASPNETCORE_ENVIRONMENT=Development
appsettings.Linux.json ASPNETCORE_ENVIRONMENT=Linux only (never OS-auto)
appsettings.Local.json Always (appended explicitly, highest file layer)

There is no appsettings.Production.json.

SystemSettings table

A SystemSetting row is Key (PK, string), Value (string), Description (string?), Group (string, default General). Values are always strings; services parse them (int, bool, or JSON) on read. There is no cache — each read opens a fresh context, so a saved change is visible to the next read.

Seeded rows

Twelve rows ship via HasData. Groups in use: Projects, Knowledge, Containers, Features.

Key Default Group Live?
DefaultProjectPath (empty) Projects Live — empty so the file value wins
WorkspaceRootPath (empty) Projects Live — empty so the file value wins
GlobalKnowledgePath (empty) Knowledge Live — empty so the file value wins
Container_IdleTimeoutMinutes 60 Containers Live
Container_DestroyGracePeriodMinutes 30 Containers Live
Container_CleanupIntervalMinutes 5 Containers Live
Container_AutoSnapshotBeforeCleanup true Containers Live
Container_MaxSnapshotsPerProject 5 Containers Live
Container_SnapshotRetentionDays 30 Containers Live

Read but not seeded

These keys are read at runtime and created on first save from the Settings page (until then, code falls back to a built-in default):

Key Fallback when unset Controls
AI_ModelId (skipped in chain) Operator-wide default model
AI_WorkerModelId (skipped) Default for non-supervisor workers
AI_SupervisorModelId falls back to AI_ModelId Layer-2 gap-detection model
AI_Layer2WorkerModel inherits parent Spawned research-subagent model
AI_BaseUrl http://localhost:1234 LM Studio chat-path endpoint
LMStudio_MultiInstanceConfig built-in defaults Multi-instance routing config (JSON)
SearXNG_Url DuckDuckGo fallback web_search backend
MaintenanceMode false Gates create/build POST routes (503 when true)
ToolApproval_TimeoutSeconds 300 Restricted-tool approval window
Finishing_DirectorModel template's pinned model Model for the finishing team's director and designer
Finishing_DirectorInstance template's pinned instance Instance slot those two run on
Finishing_WorkerModel template's pinned model Model for the finishing team's researchers and writer
Finishing_WorkerInstance template's pinned instance Instance slot those run on. Pinning a model without its instance leaves the node on the primary slot, which is how a whole team ends up on one card

Keys by concern

Each table: key · type · default · layer · live/inert · what it controls. Defaults shown are the committed appsettings.json values unless noted; deployment-specific hosts and secrets are rendered as placeholders.

Hosting, logging, connection strings

Key Type Default Layer Live? Controls
ConnectionStrings:DefaultConnection string Host=localhost;Port=5432;Database=aetherium_refactor_db;Username=aetherium_user;Password=<override locally> appsettings Live Npgsql/EF Core connection
Logging:LogLevel:Default string Information appsettings Live Default log level
Logging:LogLevel:Microsoft.AspNetCore string Warning appsettings Live ASP.NET log level
AllowedHosts string * appsettings Live Host-header filtering
ApiBaseUrl string http://localhost:5257 appsettings Live Base address for the server's own HttpClient (self-calls)
DetailedErrors bool true (Dev) appsettings Live Detailed error output in responses
BackendBaseUrl string http://localhost:5000 (code fallback) env/Local Live if set Not in any appsettings file
FrontendBaseUrl string http://localhost:5173 (code fallback) env/Local Live if set Not in any appsettings file

Storage & paths

Key Type Default Layer Live? Controls
Aetherium:Storage:WorkspaceRoot string ${AETHERIUM_WORKSPACE_ROOT} → platform default appsettings Live Workspace tier root
Aetherium:Storage:GlobalKnowledgePath string ${AETHERIUM_KNOWLEDGE_PATH} → platform default appsettings Live Global knowledge library
Aetherium:Storage:ArchivePath string ${AETHERIUM_ARCHIVE_PATH} → platform default appsettings Live Archive root
Aetherium:Storage:TempPath string ${AETHERIUM_TEMP_PATH} → platform default appsettings Live Temp path
Aetherium:Storage:GitHubToken string (empty) appsettings Live PAT for knowledge-pack downloads
WorkspaceRootPath / DefaultProjectPath / GlobalKnowledgePath string (empty) SystemSettings Live Runtime overrides for the above; empty defers to file

AI & models (runtime chat/routing path)

These are SystemSettings rows, not file config. See the model-resolution chain in §09.

Key Type Default Layer Live? Controls
AI_ModelId string (unset) SystemSettings Live Operator-wide default model
AI_WorkerModelId string (unset) SystemSettings Live Non-supervisor worker default
AI_SupervisorModelId string (unset) SystemSettings Live Layer-2 gap-detection model only
AI_Layer2WorkerModel string (unset) SystemSettings Live Spawned research-subagent model
AI_ReflectionEnabled bool false SystemSettings Live Master toggle for agent self-critique (reflection); off by default so benchmark output stays unrefined
AI_VerificationEnabled bool false SystemSettings Live Enables step verification for teams without a template verification block (the block, when present, wins — a block with enabled: false opts the team out); fail-closed — a settings error leaves verification off
AI_VerifierModelId string (falls back to AI_SupervisorModelId → AI_ModelId) SystemSettings Live Judge model for step verification (per-team verifier_model_id overrides)
AI_TrajectoryJudgeModelId string (falls back to the team's verifier_model_id, then AI_SupervisorModelId) SystemSettings Live Judge model for the end-of-run trajectory evaluation. A template's verification.trajectory_judge_model_id (and trajectory_judge_preferred_instance) overrides it, and where the template names only a step verifier the trajectory judge follows that — before this the run-end judge read the global setting alone, so a team that pinned its verifier to one model had its trajectory judged by another
AI_MaxVerificationRetries int (unset → per-team max_retries, default 2) SystemSettings Live Global cap on verify→fix bounce cycles per step
AI_StepRounds int 1 (clamped to 1–8) SystemSettings Live Fresh attempts a plan step may get: each round is a new history-free delegation of the same step plus a validated handoff from the previous round (files written, tools used, summary, verifier findings). 1 is the original single attempt; raising it changes run behaviour, so results are not comparable across the change. A team's own step_rounds (a template field or a create override) takes precedence over this setting
AI_StepRepairRounds int 2 (clamped to 0–4) SystemSettings Live Extra rounds granted only when a step failed a deliverable gate — a typed absence or a missing required_output — on top of AI_StepRounds. The two answer different questions: AI_StepRounds is how many passes the work takes and is 1 for most teams, while a repair round acts on a gate message that already names the missing path. When that message names some of the step's files, the step's other files that already meet their conditions are held as they are, so the round works on the named one. 0 restores the pre-repair behaviour
AI_RepetitionRuleArmed bool false SystemSettings Live — the deterministic repetition rule always writes [REP-RULE] lines for a looping tool call; true lets it bounce the agent as well, at most twice per loop. Off by default
AI_ImageExampleArm string (unset → arm A) SystemSettings Live, read per tool loop B swaps the example in comfyui_generate's workflow description for one without the "8k photorealistic" booster, for an A/B before the description itself changes. Unset or any other value leaves the shipped schema byte for byte. A loop on arm B logs [IMAGE-ARM]
AI_InstanceAvailabilityCheck bool true SystemSettings Live Feature flag for the scheduler's instance-availability layer, so a busy alternate instance holds its tasks rather than failing them. Set false to disable the layer without a redeploy
AI_TrajectoryJudgeModelId string (falls back to AI_SupervisorModelId → AI_ModelId) SystemSettings Live Model that judges a run's trajectory for the eval / promotion gate
AI_PromotionScoreThreshold int 7 SystemSettings Live Minimum trajectory score (1–10) to promote a template to core
AI_TrajectoryEvalOnRunComplete bool false SystemSettings Live Score every finalized run's trajectory in the background; off by default so a benchmark host does not pay a judge call per run
AI_ReasoningEffort string (unset → off) SystemSettings Live low | medium | high | xhigh: turns a thinking model's reasoning on for the two planning turns only (plan creation and the Dynamic-parallel lead's planning phase); every other turn keeps reasoning off unless the team's own config.reasoning_effort turns it on for that team's worker turns. Any other value is treated as unset
AI_InferenceLeaseIdleMinutes int (unset → Aetherium:VramOrchestration:InteractiveIdleMinutesDefault, 15) SystemSettings Live (read every 30 s) How long an interactive inference lease (the /v1 relay's chat client) keeps its model pinned against batch eviction after its last request
AI_BaseUrl string http://localhost:1234 (code fallback) SystemSettings Live LM Studio chat endpoint
LMStudio_MultiInstanceConfig string (JSON) (unset) SystemSettings Live Per-slot runtime rows: endpoints, enabled flags, per-slot concurrency. Primary and Secondary sit under the fixed keys primary and secondary; every other slot sits under instances, a row the Engine builds from that slot's registry entry the first time it is read (its endpoint is the registry's InferenceUrl), and the key is omitted when there are none
Aetherium:AI:BaseUrl string http://localhost:1234 (fallback) — Inert Orphan — read only by the health endpoint; absent from every appsettings file

LM Studio (model-management / VRAM path)

Separate from the chat path above. This appsettings block drives model load/unload and VRAM orchestration, not where inference requests are sent. Primary and Secondary are always slots; every other key under Instances adds one, and a key starting with _ is a hidden slot the arbiter uses but nothing lists, routes to or fails over to. Each slot names its backend: LmStudio (the default) manages models through LM Studio's native API; LlamaServer drives a llama.cpp llama-server in router mode (/models, /models/load, /models/unload), where ManagementUrl is the server root and the model's context length is a property of its preset rather than a load-time request.

Key Type Default Layer Live?
Aetherium:LMStudio:Instances:Primary:InferenceUrl string http://localhost:1234/v1 appsettings Live
Aetherium:LMStudio:Instances:Primary:ManagementUrl string http://localhost:1234/api/v1 appsettings Live
Aetherium:LMStudio:Instances:Primary:Backend LmStudio | LlamaServer LmStudio appsettings Live
Aetherium:LMStudio:Instances:Primary:MaxVramGB int 16 appsettings Live
Aetherium:LMStudio:Instances:Primary:Description string (free-text label) appsettings Live
Aetherium:LMStudio:Instances:Secondary:InferenceUrl string http://<secondary-host>:1234/v1 appsettings Live
Aetherium:LMStudio:Instances:Secondary:ManagementUrl string http://<secondary-host>:1234/api/v1 appsettings Live
Aetherium:LMStudio:Instances:Secondary:Backend LmStudio | LlamaServer LmStudio appsettings Live
Aetherium:LMStudio:Instances:Secondary:MaxVramGB int 16 appsettings Live
Aetherium:LMStudio:Instances:Secondary:Description string (free-text label) appsettings Live
Aetherium:LMStudio:Instances:<slot>:InteractiveCopies map Secondary: {"gemma-4-e4b-it": "gemma-interactive"} appsettings Live — model id → the identifier of a second loaded copy that serves interactive relay traffic, so external chat clients keep their own prompt cache apart from engine runs. A request the copy cannot serve is re-sent to the base model
Aetherium:LMStudio:Instances:<slot>:Enabled bool? (unset) code (absent from files) Live — false takes the slot out of service: it is never a failover alternate and its runtime row's enabled flag cannot switch it back on. Unset leaves that to the runtime row
Aetherium:LMStudio:Instances:<slot>:MaxConcurrentRequests int? (unset → 1) code (absent from files) Live — seeds the runtime row of a slot that has none yet; a stored row's own value wins after that
Aetherium:LMStudio:Instances:<slot>:RestoreDelaySeconds int 0 code (absent from files) Live — seconds the arbiter waits after the slot's last lease ends before restoring its standard set, restarted by every release; 0 restores at once, inline with the release. Above 0 a run or job that starts inside the window finds the card as the last one left it, with no reload to evict
Aetherium:LMStudio:Instances:<slot>:Card string? (unset) code (absent from files) Live — the physical card the slot runs on, a key of Aetherium:Gpus:Cards. Unset for a slot on another machine, unless that machine's card is listed with a Host. The planner prices the slot's loads with that card's costs and headroom
Aetherium:LMStudio:Instances:<slot>:Machine string? (unset) code (absent from files) Live — display only: the machine the slot's server runs on, which the Inference page groups slots by. Set it for a slot on another machine reached through a local port forward (its URL is loopback); unset, a slot on a card with no Host or at a loopback URL is this host and any other URL names its host
Aetherium:LMStudio:Instances:<slot>:Unit string? (unset) code (absent from files) Live — the systemd user unit that serves the slot (a llama-server router on the card's machine: this host, or the card's Host); scripts/render-gpu-env.py writes its card-pin environment file from Card and GpuApi, and the unit does not start without one
Aetherium:LMStudio:Instances:<slot>:GpuApi Hip | Vulkan Hip code (absent from files) Live (read by scripts/render-gpu-env.py, not the Engine) — which llama.cpp build serves the slot, and so how its unit is pinned: by ROCm UUID (Hip) or by Mesa device id (Vulkan)
Aetherium:LMStudio:SlotOrder list of slot (empty → Primary, Secondary, then the rest by name) code (absent from files) Live — catalog order of the slots: the order failover, smart routing, the team generator and the judges' residency hint try slots in, and the relay looks for a model in. A slot not named follows the named ones
Aetherium:LMStudio:ProbeTimeoutSeconds int 15 code (absent from files) Live — ceiling on a read-only listing against a slot (load state, residency ledger, catalog); a listing that has not answered in time is a failed probe. Loads and unloads keep the management client's 120 s timeout. Connecting is bounded separately — 5 s on the management and /v1 relay clients, 10 s on the default client — so a server whose machine drops the connection attempt fails in seconds; generation time is not affected
Aetherium:RolePlacement:<role> {Slot, Pin} (empty) code (absent from files) Live — the slot for a model call that belongs to no team node. Roles: Compaction, Summaries, Lessons, CodeReview, TestDrive, DecisionLayer, Fork, Discovery, Verifier, TrajectoryJudge, TeamGenerator, ImagePrompt, Embeddings. Pin: true makes the slot binding (the task fails rather than move); unpinned it is a hint. A role not named keeps its built-in default (Primary for most); a template's own verifier or judge instance, and a fork's own instance, still win. Unknown roles and unknown slots are logged at start-up as [ROLE-PLACEMENT]
Aetherium:LMStudio:OfflineFallbackSlot string? (unset) code (absent from files) Live — where an unpinned side role (an Aetherium:RolePlacement role, or a call site's built-in Primary default) runs while its own slot is known down: the slot's last two health probes (30 s apart) failed and the latest is under two minutes old. A known-down slot also refuses leases at once and is skipped by smart routing and the re-warm loop. A pinned role and every team node stay where they are and fail fast, and nothing moves while the fallback slot is itself down. A role works there only when that slot serves its model. Unset, side roles stay on their slot and fail. Each move is logged [ROLE-PLACEMENT] (at most every five minutes per slot). The team generator also tries this slot's resident model when its own unpinned placement slot holds none, before Primary
Aetherium:LMStudio:DefaultContextLength int 49152 appsettings Live
Aetherium:LMStudio:ModelLoadTimeoutSeconds int 120 appsettings Live
Aetherium:LMStudio:ModelUnloadWaitSeconds int 5 appsettings Live
Aetherium:LMStudio:ModelUnloadVerifyGraceSeconds int 15 appsettings Live — after the unload wait, how long the verify keeps polling for the model to leave before the unload counts as failed (a backend lets an in-flight request finish first)
Aetherium:LMStudio:VerifyAfterLoadUnload bool true appsettings Live

VRAM orchestration

Key Type Default Layer Live?
Aetherium:VramOrchestration:Enabled bool true appsettings Live
Aetherium:VramOrchestration:PreWarmEnabled bool true code (absent from files) Live — loads each slot's standard set at start-up, and every 30 s re-warms a LlamaServer slot with a standard set that has answered empty on two consecutive checks with no lease and nothing in flight (its server restarted under it). MaintenanceMode suspends the re-warm, so a router emptied by hand without a lease comes back within about a minute unless a lease or maintenance mode keeps it empty
Aetherium:VramOrchestration:HeadroomGB double 1.5 appsettings Live — VRAM the planner will not spend, so a run cannot starve the desktop
Aetherium:VramOrchestration:HeadroomGBByCard map of card → double (empty) code (absent from files) Live — headroom for slots on that card, winning over HeadroomGB (a card that drives a display needs more than one that drives nothing)
Aetherium:VramOrchestration:UnknownOverheadGB double 0.75 appsettings Live — added to the file size of a model with no measured cost
Aetherium:VramOrchestration:UnknownKvBytesPerToken int 24576 appsettings Live — per-token KV allowance for an unmeasured model
Aetherium:VramOrchestration:StandardModels map of slot → list Primary/Secondary sets appsettings Live — what each instance carries between leases: loaded at start-up, evicted when a lease needs the room (unless the slot is in ProtectedStandardSetSlots), restored by the arbiter's sweep once no lease is live or queued on the slot
Aetherium:VramOrchestration:DefaultNodeInstance string Worker appsettings Live — at run start, a node (or subagent_defaults) that names a model but no preferred_instance is moved to this slot when the slot's standard set holds exactly one non-embedding model its id matches, and its model_id is rewritten to that id, since a llama-server slot serves only its preset ids; each move is logged [P4-HOME]. A model the slot does not hold, a pinned node and a node with no model are left alone, and the key is ignored when no slot by this name is registered. Unset, such a node runs on Primary
Aetherium:VramOrchestration:ModelCosts map of model → {BaseBytes, KvBytesPerToken} measured entries appsettings Live — measured VRAM cost; an unlisted model falls back to the two keys above. KvBytesPerToken: 0 records a KV cache held in system RAM rather than on the GPU, which for some models is a load-time choice, not their default — if that placement is not also forced at load time the entry describes a configuration the Engine never loads. Lookup tolerates an org prefix and a @quant suffix, so two quantisations of one model share an entry
Aetherium:VramOrchestration:ModelCostsByCard map of card → model → {BaseBytes, KvBytesPerToken} (empty) code (absent from files) Live — measured costs that differ by card (compute buffers, a HIP or Vulkan build); an entry wins over ModelCosts for a slot on that card. Keyed by card, so moving a slot to another card needs no cost edits
Aetherium:VramOrchestration:PreemptGraceSeconds int 20 code (absent from files) Live — how long a preempted batch lease has to release itself before its model is unloaded anyway
Aetherium:VramOrchestration:LeaseSweepSeconds int 5 code (absent from files) Live — period of the lease sweep: expiry, granting queued leases, restoring an idle slot
Aetherium:VramOrchestration:RunLeaseTtlHours int 8 code (absent from files) Live — a run lease that was never released (the run died) expires after this
Aetherium:VramOrchestration:InteractiveIdleMinutesDefault int 15 code (absent from files) Live — interactive lease idle window when AI_InferenceLeaseIdleMinutes is unset
Aetherium:VramOrchestration:BatchLeaseDefaultMinutes int 30 code (absent from files) Live — TTL of a batch lease that names none
Aetherium:Tts:BaseUrl string http://127.0.0.1:8100/v1 code (absent from files) Live — the voice service tts_render posts to; {BaseUrl}/audio/speech, health at the same host's /health
Aetherium:Tts:RefsDir string ~/.cache/dnd-models/tts-qwen/refs code (absent from files) Live — reference clips with transcripts the voice tools clone
Aetherium:Tts:DefaultVoice string host code (absent from files) Live
Aetherium:Tts:GapSeconds double 0.18 code (absent from files) Live
Aetherium:Tts:LineTimeoutSeconds int 300 code (absent from files) Live — per-line synthesis timeout
Aetherium:Tts:DesignBaseUrl string http://127.0.0.1:8101/v1 code (absent from files) Live — the VoiceDesign server (the qwen-tts-design GPU service) tts_render asks once per described voice; the clip is saved under {RefsDir}/designed/
Aetherium:Tts:RelayVoices map of voice name → reference stem (empty) appsettings Restart — the voices POST /v1/audio/speech speaks in: an OpenAI voice name → <stem>.wav + <stem>.txt under RefsDir. A name not in the map is a 400
Aetherium:Tts:RelayService string qwen-tts code (absent from files) Restart — the GPU service the speech relay leases and renders with
Aetherium:Tts:RelayIdleMinutes int 10 code (absent from files) Restart — how long the speech relay keeps the voice service leased after its last request
Aetherium:Tts:RelayQueueWaitSeconds int 240 code (absent from files) Restart — how long a speech request waits for the lease while the card is busy before a 503
Aetherium:Tts:RelayMaxWords int 380 code (absent from files) Restart — the longest input one speech request renders (about 2.5 minutes); longer is a 400, never a truncation
Aetherium:Tts:RelayRenderBaseSeconds int 30 code (absent from files) Restart — the fixed part of one speech render's budget; a render with no audio after this plus RelayRenderSecondsPerWord per input word (never more than LineTimeoutSeconds) is cut off with a 504
Aetherium:Tts:RelayRenderSecondsPerWord double 0.5 code (absent from files) Restart — the per-word part of the render budget above
Aetherium:VramOrchestration:Services map of name → {Unit, Slot, Bytes, HealthUrl?, StartTimeoutSeconds, LingerSeconds, Env, EnvByCard, AloneEnv?} (empty) appsettings Live — GPU services a lease may name (a systemd user unit): the arbiter starts the unit under the lease, plans Bytes as a resident of that slot, waits for HealthUrl, and stops the unit when no live lease names it. LingerSeconds (default 0: stop at once) keeps an unreferenced unit warm that long; a lease that needs nothing new on the card reuses it, any lease or mid-run load that must load or start something stops every warm unit on the slot first, and the standard set is restored only once they have stopped. Env (and EnvByCard, card → variables laid over it when the service's slot is on that card) are extra variables scripts/render-gpu-env.py writes into the unit's environment file beside the card pin. AloneEnv, when set, makes that cap and Bytes apply only while a model shares the card: a lease that names no model and exactly one service, on a card no other lease pins a model on and no other service runs, starts the unit with AloneEnv instead and books the card's whole budget, so nothing loads beside it (a whole-card booking the planner refuses falls back to the capped one). The variables reach the unit through a per-start file, ~/.config/aetherium/gpu/<unit>.start.env, which the shipped comfyui-qwen unit reads; a unit without that file ignores AloneEnv
Aetherium:VramOrchestration:ProtectedStandardSetSlots list of slot ["Secondary"] appsettings Live — slots whose standard set no lease may evict; a batch lease there must fit beside it
Aetherium:VramOrchestration:ServiceStartMaxCelsius double 0 (off) code (absent from files) Live — a run or batch lease that would start a GPU service on a slot of the Engine's own machine (a slot with no card, or whose card has no Host) waits in the queue while the CPU package temperature (k10temp Tctl) is above this, with the reading as its waiting reason, and starts once it is at or under it. A service already running is reused whatever the reading; interactive leases never start services. No sensor means no gate. For machines whose GPU shares the die with the CPU
Aetherium:VramOrchestration:CoResidentLmStudio map of slot → hidden instance name (empty) appsettings Live — an LM Studio sharing a llama-server slot's card (an instance whose name starts with _ is hidden from every listing): whatever it holds is unloaded before the arbiter loads on that slot and reported as a foreign occupant. A slot with no entry here takes the hidden LM Studio instance whose Card is the slot's card
Aetherium:Gpus:Cards map of name → {Uuid, VulkanDevice?, DrivesDisplay, Host?, ListenAddress?, Description} (empty) code (absent from files) Live — the physical cards, named by slots' Card. Uuid is the ROCm UUID (GPU-<hex>); VulkanDevice (vendor:device) is needed by a slot served by a Vulkan build. Card telemetry (GET /api/inference/cards) reads them; a slot naming an unknown card, a card with no Uuid, and one unit serving two slots are logged at start-up as [GPU-CARDS]. scripts/render-gpu-env.py refuses to pin a unit whose card is missing, has no Uuid, or is absent from sysfs. Aetherium:Gpus:Builds:{Hip,Vulkan} names each build's llama-server binary (default ~/llama-cpp/current/llama-server and ~/llama-cpp/vulkan-current/llama-server) and is read by the script only. Host (user@address; unset = the box the Engine runs on) puts a card in another machine: the arbiter starts and stops the GPU services on its slots over SSH through that machine's services/aetherium-gpu-node wrapper — a host that does not answer counts its services as stopped and is not asked again for 30 s — and scripts/deploy.sh validates and renders the card's pins on that machine, against its sysfs, and restarts the card's routers there. ListenAddress (read by the script only) is the address that card's routers and GPU services bind, written into each pin file as AETHERIUM_LISTEN; it defaults to 127.0.0.1, and a card with a Host must set it
Aetherium:Gpus:RemoteSshKey string? (unset) code (absent from files) Live — the identity file the Engine uses for SSH to a card's Host (~/ is expanded). On the GPU host its authorized_keys line forces services/aetherium-gpu-node, which runs only is-active, start, stop and write-env on the service units the deploy published there, and cards (that machine's GPUs from sysfs, for GET /api/inference/cards). Use a dedicated key, not one of ssh's default identity files: deploy.sh's own SSH uses those, and a default key forced to the wrapper would land the deploy in it. Unset: ssh's own defaults and ~/.ssh/config

Devices (Aetherium:Devices:SteamFrame)

Where the Steam Frame headset is and how the Engine's host reaches it, for the device tools (frame_status, frame_install, frame_run, frame_grab). The section is absent from the shipped files, so every key is its code default until set in appsettings.Local.json. Each host pairs with the headset itself; a key copied from another host is never used.

Key Type Default Layer Live?
Aetherium:Devices:SteamFrame:Host string a LAN address code (absent from files) Live — the headset's address; an IP, since the headset's name does not resolve on every host
Aetherium:Devices:SteamFrame:KeyPath string ~/.config/steamos-devkit/devkit_rsa code (absent from files) Live — this host's SteamOS Devkit key, written when the host paired with the headset
Aetherium:Devices:SteamFrame:KnownHostsPath string ~/.config/aetherium/frame/known_hosts_frame code (absent from files) Live — the headset's public host key
Aetherium:Devices:SteamFrame:StateDir string ~/.local/share/aetherium/frame code (absent from files) Live — per-package state kept off every workspace: the app's last saves, and a run's files until they are moved into the workspace
Aetherium:Devices:SteamFrame:Adb string adb code (absent from files) Live — adb on this host
Aetherium:Devices:SteamFrame:BuildsRoot string? /srv/share/steam-frame/builds code (absent from files) Live — a folder of builds an APK may also come from, besides the agent's own workspace; unset, the workspace only
Aetherium:Devices:SteamFrame:DefaultPackage / DefaultProject / DefaultActivity string com.YourCompany.PaintTheWorld / PaintTheWorld / com.epicgames.unreal.SplashActivity code (absent from files) Live — the app when a call names none
Aetherium:Devices:SteamFrame:ScriptsDir string? (unset) code (absent from files) Live — the scripts the tools run; unset, device/steam-frame beside the published app

Finishing (Aetherium:Finishing)

Key Type Default Layer Live?
Aetherium:Finishing:AutoTrigger bool false appsettings Live — when true, a build run reaching Completed opens a finishing gate for that project. Off by default and deliberately so: turning it on starts unattended design work on every project that finishes a build. "Completed" here means no failed step, no 0/N steps succeeded, no circuit-breaker stop and no infrastructure failure — it is not a plan-level verifier verdict

The models the two phases run on are SystemSettings rows rather than appsettings keys, so they can be changed without a restart — see Read but not seeded.

Image finishing (Aetherium:ImageFinishing)

Key Type Default Layer Live?
Aetherium:ImageFinishing:Enabled bool false code (absent from files) Restart — re-render every Completed Qwen-Image team's images at 2K after the run. Off since 2026-10-12: 2K images are made in a finishing run, by request
Aetherium:ImageFinishing:PromptDir string (beside the enhancer model files) code Restart — where the prompt enhancer's system prompts are read from; they are not vendored, so set this per box
Aetherium:ImageFinishing:T2IModel / I2IModel string qwen-image-pe-t2i / qwen-image-pe-i2i code Restart — the enhancer presets for the re-render and the upscale
Aetherium:ImageFinishing:Steps int 40 code Restart
Aetherium:ImageFinishing:MegaPixels double 4.0 code Restart — the re-render's size, about 2K
Aetherium:ImageFinishing:UpscaleGrid int 2048 code Restart — the upscale's long-side grid
Aetherium:ImageFinishing:MaxImagesPerRun int 12 code Restart

See finish_image for what the two passes do.

Decision layer (Aetherium:Decision)

Key Type Default Layer Live?
Aetherium:Decision:Enabled bool false appsettings Per session — when true, the worker tool loop gains a semantic-repetition guard that asks a resident model, through IDecisionModel, whether a looping agent is repeating itself, and may bounce it
Aetherium:Decision:Shadow bool false appsettings Per session — the guard asks and writes its decision_logs row but never bounces, so runs are unchanged. Implies Enabled

Research (Aetherium:Research)

Key Type Default Layer Live?
Aetherium:Research:SearchMinIntervalMs int 1500 appsettings Live — minimum spacing between outbound web_search calls, process-wide. Clamped to 0–30000; 0 keeps the gate in place and only removes the wait, so a parallel wave of researchers still queues rather than bursting every engine at once

Pacing is half the remedy for engines that suspend themselves under agent-rate search; the other half is the engine set in the SearXNG config. The engines SearXNG reports as unresponsive are now read back and logged per search, so a degraded search set is visible rather than silently returning fewer results.

Knowledge seeding

Key Type Default Layer Live?
Aetherium:KnowledgeSeeding:Enabled bool true appsettings Live
Aetherium:KnowledgeSeeding:InitialDelaySeconds int 30 appsettings Live
Aetherium:KnowledgeSeeding:MaxConcurrentJobs int 1 appsettings Inert — bound, never read
Aetherium:KnowledgeSeeding:EnableLayer2Research bool true appsettings Live
Aetherium:KnowledgeSeeding:ResearchSubagentLimit int 4 appsettings Live
Aetherium:KnowledgeSeeding:ResearchTimeoutMinutes int 10 appsettings Live

Knowledge / ChromaDB

The ChromaDb section sits at the configuration root, not under Aetherium. The Aetherium:Embeddings keys, listed with it, do sit under Aetherium. Chunking has no keys: chunker v2 packs heading sections into 400–1,600 characters with a breadcrumb title, windows code by 60 lines with 8 lines of overlap, and caps every chunk at 2,000 characters.

Key Type Default Layer Live?
ChromaDb:Endpoint string http://localhost:8000 appsettings Live
ChromaDb:DefaultCollection string aetherium_knowledge appsettings Inert — bound, never read; collection names are hardcoded
ChromaDb:ScoreFloor float 0.525 code Live — lowest cosine similarity a retrieved chunk may have; distances from every collection space are converted to cosine first. 0.525 is where the old 0.05 on 1 − d over an L2 space sat for unit vectors
ChromaDb:Image string chromadb/chroma:1.5.2@sha256:3a9bcbc6… code Live — reported by the health rag block; Chroma's own version endpoint answers 1.0.0 whatever runs
ChromaDb:IndexBuildLogs bool false appsettings Live
Aetherium:Embeddings:ModelId string text-embedding-nomic-embed-text-v1.5 code (absent from files) Live — the model id the knowledge index embeds with (and the availability canary names). LM Studio answers any name from whichever embedding model it holds; a llama-server router answers 400 for a name that is not one of its presets. The canary asks the slot of the Embeddings role placement first (only that slot when pinned), then every other enabled slot — Primary, Secondary, then the rest; one canary is shared by every request for a minute
Aetherium:Embeddings:RawModelId string text-embedding-nomic-embed-text-v1.5 code Live — the model the untemplated path names; lessons embed through it, so moving ModelId leaves stored lesson vectors comparable
Aetherium:Embeddings:PrefixScheme string nomic-v1 code Live — names the template pair; ModelId|PrefixScheme is the stamp every collection and chunk carries
Aetherium:Embeddings:QueryTemplate string search_query: {text} code Live
Aetherium:Embeddings:DocumentTemplate string search_document: {title}\n\n{text} code Live — without a title the title line is dropped
Aetherium:Embeddings:BatchSize int 32 code Live — inputs per request (array input)
Aetherium:Embeddings:MaxInputChars int 6000 code Live — longest templated input sent
AI_KnowledgeInjection string unset (on) SystemSettings Live — false turns off knowledge injection into prompts; benchmark mode always turns it off
Knowledge_ActiveGeneration int unset (0) SystemSettings Live — the generation reads and writes use: 0 is the unsuffixed names, N is aetherium_global_gN, aetherium_domain_<name>_gN, aetherium_project_<id>_gN. Read every 10 s, so a flip takes effect without a restart
Aetherium:KnowledgeIndex:WorkRoot string /var/lib/aetherium/kb-rebuild code Live — one g<N>-<utc>/ directory per rebuild: export-old.jsonl, lessons-g<N>.jsonl, manifest-g<N>.jsonl, report.json
Aetherium:KnowledgeIndex:KbRoot string /var/lib/aetherium/kb code Live — candidates/p<project>/<id>.md holds each promotion candidate's text until it is approved or rejected (a workspace cleanup can delete the project's own copy); domains/<domain>/<id>.md holds each approved document; media/<hash>.json caches each picture's caption and each recording's transcript by the file's content, and a media candidate or document keeps its file beside its text
Aetherium:KnowledgeIndex:LeaseWaitMinutes int 60 code Live — how long a build waits for its batch lease on the embedding slot
Aetherium:KnowledgeIndex:PausePollSeconds int 15 code Live — between write batches a build pauses while a run lease holds the embedding slot, re-checking at this interval
Aetherium:KnowledgeIndex:MaxDocsPerWrite / MaxCharsPerWrite int 40 / 200000 code Live — size of one embedding write batch
Aetherium:KnowledgeIndex:SelfRetrievalGate double 0.9 code Live — per pack, the share of sampled chunks that find themselves first (SelfRetrievalSamplesPerPack, 50)
Aetherium:KnowledgeIndex:KnownItemGate double 0.9 code Live — hit@5 of KnownItemQueries (60) document-title lookups, each asked within the pack set of every shipped template that names the document's pack
Aetherium:KnowledgeIndex:OverlapGate double 0.95 code Live — overlap@10 of the HNSW index against exact search, on packs up to OverlapMaxPackChunks (5000) chunks
Aetherium:KnowledgeIndex:FingerprintGate double 0.999 code Live — cosine of the probe embedding against the stamp's fingerprint
Aetherium:Media:CaptionModel string qwen/qwen3.5-9b code Live — the vision model that captions a picture for the knowledge base, as its slot's router names it
Aetherium:Media:CaptionSlot string Worker code Live — the slot the caption model loads on, under one batch lease (media-captions) per batch of pictures
Aetherium:Media:SttCommand string /data/stt/stt-local.py code Live — takes one audio file and prints its transcript; runs on the CPU, no lease
Aetherium:Media:SegmentSeconds / MaxAudioSeconds int 600 / 3600 code Live — a longer recording is cut into pieces of this length; a recording over the maximum is not transcribed
Aetherium:Media:MaxImageBytes long 20971520 code Live — a larger picture is not captioned

Tiers

The Aetherium:Tiers block sets per-tier retention and container resource limits. An unknown tier name falls back to Staging.

Key (per tier) Production Staging Temp
…:{Tier}:RetentionDays -1 (never) 30 7
…:{Tier}:MaxProjects -1 (unlimited) 50 20
…:{Tier}:ArchiveBeforeDelete true true true
…:{Tier}:Resources:CpuLimit 4.0 2.0 1.0
…:{Tier}:Resources:MemoryLimit 8g 4g 2g
…:{Tier}:Resources:DiskQuota 50g 20g 5g

All live via the tier-policy service. Resources:CpuShares (default 1024) has no appsettings key — it is default-only.

The Resources values are applied when a project's team container is created: a Production project's container gets 4.0 CPU / 8g, a Temp project's 1.0 CPU / 2g. A team that declares its own default_resources overrides the tier, but only when that declaration differs from the defaults — a block restating 2.0/4g asks for what it would already get and defers to the tier.

Containers (runtime lifecycle)

The lifecycle timers are SystemSettings rows (re-read each cleanup cycle), not the same-named Aetherium:Cleanup keys, which are shadowed (see below).

Key Type Default Layer Live?
Container_IdleTimeoutMinutes int 60 SystemSettings Live
Container_DestroyGracePeriodMinutes int 30 SystemSettings Live
Container_CleanupIntervalMinutes int 5 SystemSettings Live
Container_AutoSnapshotBeforeCleanup bool true SystemSettings Live — Production and Staging tiers only; Temp-tier projects are never snapshotted
Container_MaxSnapshotsPerProject int 5 SystemSettings Live
Container_SnapshotRetentionDays int 30 SystemSettings Live

Cleanup (Aetherium:Cleanup) — mostly shadowed

Only two keys here are read. The rest are shadowed by the Container_* SystemSettings rows above — change those, not this block.

Key Type Default Live?
Aetherium:Cleanup:ArchiveRetentionDays int 30 Live
Aetherium:Cleanup:CompressArchives bool true Live
Aetherium:Cleanup:IdleTimeoutMinutes int 60 Inert (shadowed)
Aetherium:Cleanup:DestroyGracePeriodMinutes int 30 Inert (shadowed)
Aetherium:Cleanup:CleanupIntervalMinutes int 5 Inert (shadowed)
Aetherium:Cleanup:AutoSnapshotBeforeCleanup bool true Inert (shadowed)
Aetherium:Cleanup:MaxSnapshotsPerProject int 5 Inert (shadowed)
Aetherium:Cleanup:SnapshotRetentionDays int 30 Inert (shadowed)

Bridge / OpenClaw / webhooks

Key Type Default Layer Live?
Aetherium:Bridge:Enabled bool true appsettings Live
Aetherium:Bridge:PublicHost string <deployment host/IP> appsettings Live — set per deployment
Aetherium:Bridge:AppUrl string? (unset) appsettings Live — the app's address (https://<host>/app) that operator notices link to; unset, a notice names the page instead
Aetherium:Bridge:PortAllocation:RangeStart int 3000 appsettings Live
Aetherium:Bridge:PortAllocation:RangeEnd int 3499 appsettings Live
Aetherium:Bridge:PortAllocation:ScanTimeout int 5 appsettings Live
Aetherium:Bridge:PortAllocation:Tiers:Production:Start / :End int 3000 / 3099 appsettings Live
Aetherium:Bridge:PortAllocation:Tiers:Staging:Start / :End int 3100 / 3199 appsettings Live
Aetherium:Bridge:PortAllocation:Tiers:Temp:Start / :End int 3200 / 3499 appsettings Live
Aetherium:Bridge:Webhooks:Targets[]:Name string <target-name> appsettings Live
…:Targets[]:Url string http://localhost:18790/hooks/agent appsettings Live
…:Targets[]:Secret string REPLACE_ME_VIA_LOCAL_OVERRIDE appsettings Live — HMAC secret; never commit a real value
…:Targets[]:Events string (JSON array) ["build.*", "tool.approval.requested"] appsettings Live
…:Targets[]:Enabled bool true appsettings Live
…:Targets[]:Kind string? (unset) appsettings Live — unset for a webhook listener; telegram posts each event's message to the Bot API ({Url}/bot{Secret}/sendMessage, Url defaulting to https://api.telegram.org) and skips events without one
…:Targets[]:ChatId string? (unset) appsettings Live — the chat a telegram target sends to

Webhook targets are dual-sourced: this config block plus the webhook_targets table, merged at read time.

Maintenance (self-maintenance source mount)

Key Type Default Layer Live?
Aetherium:Maintenance:AetheriumSourcePath string ${AETHERIUM_SOURCE_PATH} (env-expanded; environment overlays may pin an absolute path) appsettings Live
Aetherium:Maintenance:ReadOnly bool false appsettings Live

The host path of the Engine's own source checkout, mounted at /aetherium-src inside containers whose template sets mount_aetherium_source: true (the maintenance and quick-fix templates). A blank or unresolved path skips the mount with a warning rather than guessing — a wrong path would mount a stale tree and silently break self-maintenance. ReadOnly: false is deliberate: the upgrade engineer edits the tree, and the restricted-command approval gate is the safety layer for anything that builds or deploys from it.

Security & permissions

Key Type Default Layer Live?
Aetherium:PermissionRulesPath string {BaseDir}/config/permissions.yaml code (absent from files) Live
Aetherium:ToolCatalog:FailOnDrift bool true code (absent from files) Live
ToolApproval_TimeoutSeconds int 300 SystemSettings Live (unseeded)
allowed_domains list (empty → DLP only) Template (per team) Live
Permission rules YAML rules: [] (empty) config/permissions.yaml Live (hot-reloaded; always_deny enforced)

Inert sections (defined, never read)

Setting any key in these sections has no effect today.

Section Keys Why inert
Gemini:ApiKey (single key) Present in appsettings.json, never read anywhere.

Removed sections

Aetherium:Docker:* and Aetherium:Pools:* were bound but never read; both sections and their option classes were removed on 2026-09-06. A key left under either name in a local override is now simply ignored.

Hardcoded literals (no config key)

Values that look configurable but are baked into code. Candidates for promotion to keys; flagged so you don't hunt for a knob that doesn't exist.

Value Location Notes
Default container image python:3.11-slim Models/DockerImageCatalog.cs No key; a template's base_image overrides it per project
Approval timeout 300 s Services/ToolApprovalService.cs Default when ToolApproval_TimeoutSeconds is unset
Egress always-allowed hosts Services/EgressPolicyService.cs localhost, 127.0.0.1, ::1, host.docker.internal — no key

Audit retention is unbounded

The tool-security audit service writes every gated call to PostgreSQL with no retention, purge, or config key — the audit table grows without limit. Prune it yourself if you run the Engine continuously.

Option classes

The POCO classes that bind configuration sections, for code-side lookup.

Class Section Status
AetheriumOptions Aetherium Live (Tiers; Cleanup partial)
StorageOptions Aetherium:Storage Live (+ post-configure path expansion)
TierOptions / ResourceLimits Aetherium:Tiers[*] Live
CleanupOptions Aetherium:Cleanup Partial — only ArchiveRetentionDays + CompressArchives
ChromaDbOptions ChromaDb (root) Partial — DefaultCollection inert
LMStudioOptions Aetherium:LMStudio Live
VramOrchestrationOptions Aetherium:VramOrchestration Live
ImageFinishingOptions Aetherium:ImageFinishing Live
KnowledgeSeedingOptions Aetherium:KnowledgeSeeding Partial — MaxConcurrentJobs inert

SignalR and the named HttpClients are configured inline in Program.cs with hardcoded values (SignalR client timeout 10 min, keep-alive 15 s, max receive 10 MB; webhook HttpClient 60 s; LM Studio management 120 s, with read-only slot listings cut off at Aetherium:LMStudio:ProbeTimeoutSeconds; ChromaDB 30 s; the /v1 inference relay has no timeout, so a streamed reply is bounded by the client) — not key-driven.

Where to go next